feat: Add models for content

This commit is contained in:
Robert Prehn 2020-07-20 22:04:04 +00:00
parent e73f2c8996
commit 36589ecfee
54 changed files with 2699 additions and 0 deletions

34
apps/content/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
auth_web-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/

View file

@ -0,0 +1,26 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
use Mix.Config
# General application configuration
config :content,
ecto_repos: [Content.Repo]
config :phoenix, :json_library, Jason
# Configures Elixir's Logger
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:user_id]
config :content, Content.Scheduler,
jobs: [
{"@hourly", {ContentWeb.Sitemaps, :generate, []}}
]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"

View file

@ -0,0 +1,76 @@
use Mix.Config
# Configure your database
config :content, Content.Repo,
username: "postgres",
password: "postgres",
database: "content_dev",
hostname: "localhost",
show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
config :content, ContentWeb.Endpoint,
http: [port: 4000],
debug_errors: true,
code_reloader: true,
check_origin: false,
watchers: [
node: [
"node_modules/webpack/bin/webpack.js",
"--mode",
"development",
"--watch-stdin",
cd: Path.expand("../assets", __DIR__)
]
]
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
# mix phx.gen.cert
#
# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
# https: [
# port: 4001,
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
# ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Watch static and templates for browser reloading.
config :content, ContentWeb.Endpoint,
live_reload: [
patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$",
~r"lib/content_web/(live|views)/.*(ex)$",
~r"lib/content_web/templates/.*(eex)$"
]
]
# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

View file

@ -0,0 +1,12 @@
use Mix.Config
# Print only warnings and errors during test
config :logger, level: :warn
config :content, Content.Repo,
username: "postgres",
password: "postgres",
database: "content_test#{System.get_env("MIX_TEST_PARTITION")}",
hostname: System.get_env("DATABASE_URL") || "localhost",
show_sensitive_data_on_connection_error: true,
pool: Ecto.Adapters.SQL.Sandbox

View file

@ -0,0 +1,34 @@
defmodule Content.Application do
@moduledoc """
The base module of the CMS application.
"""
use Application
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Content.Repo, []),
# Start the endpoint when the application starts
# Start your own worker by calling: Content.Worker.start_link(arg1, arg2, arg3)
# worker(Content.Worker, [arg1, arg2, arg3]),
worker(Content.Scheduler, []),
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Content.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
def config_change(changed, _new, removed) do
Endpoint.config_change(changed, removed)
:ok
end
end

View file

@ -0,0 +1,40 @@
defmodule Content.Attachment do
@moduledoc """
Helpers for dealing with "attachment"-type posts, which are generally media
uploaded to the site e.g. images.
"""
alias Content.Post
def dimensions(attachment) do
meta =
attachment
|> Post.metas_map
deserialization_results =
meta["_wp_attachment_metadata"]
|> PhpSerializer.unserialize
case deserialization_results do
{:ok, info} ->
%{
width: info |> Enum.find(fn {key, _} -> key == "width" end) |> elem(1),
height: info |> Enum.find(fn {key, _} -> key == "height" end) |> elem(1)
}
{:error, _} ->
nil
end
end
def vertical?(attachment) do
case dimensions(attachment) do
%{width: width, height: height} ->
if width < height do
true
else
false
end
_ ->
false
end
end
end

View file

@ -0,0 +1,178 @@
defmodule Content.UpdateMenu do
alias Content.{Menu, Post, Postmeta, Repo, TermRelationship}
alias Ecto.Multi
import Ecto.Query
def run(id, new_menu_params) do
current_posts = Menu.nav_menu_items_for_id(id)
post_ids_in_new_menu = recursive_post_ids(new_menu_params)
deleted_post_ids =
current_posts
|> Enum.reject(& &1."ID" in post_ids_in_new_menu)
|> Enum.map(& &1."ID")
Multi.new()
|> process_nodes(id, 0, new_menu_params |> add_order())
|> Multi.delete_all(:stale_nodes, from(p in Post, where: p."ID" in ^deleted_post_ids))
|> Repo.transaction()
end
def add_order(tree) do
{_next_order, nodes} = add_order_starting_at(tree, 1)
nodes
end
def add_order_starting_at(tree, starting_at) do
tree
|> Enum.reduce({starting_at, []}, fn node, {order, new_nodes} ->
{next_order, new_child_nodes} = add_order_starting_at(node["children"], order + 1)
new_node =
node
|> Map.merge(%{
"order" => order,
"children" => new_child_nodes,
})
{next_order, new_nodes ++ [new_node]}
end)
end
defp process_nodes(multi, menu_id, parent_id, nodes) do
nodes
|> Enum.reduce(multi, fn node, m ->
case node["post_id"] do
nil ->
create_node(m, menu_id, parent_id, node)
_id ->
update_node(m, menu_id, parent_id, node)
end
end)
end
defp create_node(multi, menu_id, parent_id, node) do
post =
Post.changeset(
%Post{},
%{
post_author: 1,
post_title: node["title"],
post_status: "publish",
comment_status: "closed",
ping_status: "closed",
menu_order: node["order"],
post_type: "nav_menu_item",
comment_count: 0,
}
)
step_name = "#{parent_id}.create_node.#{node["order"]}"
multi
|> Multi.insert(step_name, post)
|> Multi.run("#{step_name}.term_relationship", fn _repo, %{^step_name => post} ->
tr =
TermRelationship.changeset(
%TermRelationship{},
%{
object_id: post."ID",
term_taxonomy_id: menu_id,
term_order: 0,
}
)
Repo.insert(tr)
end)
|> Multi.merge(fn %{^step_name => post} ->
Multi.new()
|> insert_metas(type_of_node(node), post, parent_id, node)
end)
|> Multi.merge(fn %{^step_name => post} ->
Multi.new()
|> process_nodes(menu_id, post."ID", node["children"])
end)
end
defp insert_metas(multi, "post", post, parent_id, node) do
multi
|> update_meta(post."ID", "_menu_item_type", "post_type")
|> update_meta(post."ID", "_menu_item_object", "page")
|> update_meta(post."ID", "_menu_item_object_id", node["target_id"])
|> update_meta(post."ID", "_menu_item_menu_item_parent", parent_id)
end
defp insert_metas(multi, "category", post, parent_id, node) do
multi
|> update_meta(post."ID", "_menu_item_type", "taxonomy")
|> update_meta(post."ID", "_menu_item_object", "category")
|> update_meta(post."ID", "_menu_item_object_id", node["target_id"])
|> update_meta(post."ID", "_menu_item_menu_item_parent", parent_id)
end
defp insert_metas(multi, "link", post, parent_id, node) do
multi
|> update_meta(post."ID", "_menu_item_type", "custom")
|> update_meta(post."ID", "_menu_item_object", "custom")
|> update_meta(post."ID", "_menu_item_object_id", post."ID")
|> update_meta(post."ID", "_menu_item_url", node["url"])
|> update_meta(post."ID", "_menu_item_menu_item_parent", parent_id)
end
defp type_of_node(%{"url" => url}) when url != nil, do: "link"
defp type_of_node(%{"related_item" => %{"resource" => "posts"}}), do: "post"
defp type_of_node(%{"related_item" => %{"resource" => "category"}}), do: "category"
defp update_node(multi, menu_id, parent_id, node) do
multi
|> update_meta(node["post_id"], "_menu_item_menu_item_parent", parent_id)
|> update_order(node["post_id"], node["order"])
|> process_nodes(menu_id, node["post_id"], node["children"])
end
defp update_meta(multi, post_id, meta_key, new_value) when is_integer(new_value),
do: update_meta(multi, post_id, meta_key, new_value |> Integer.to_string())
defp update_meta(multi, post_id, meta_key, new_value) do
step_name = "#{post_id}.update_meta.#{meta_key}"
type = Postmeta.__schema__(:type, :meta_value)
cast_value = Ecto.Type.cast(type, new_value)
Postmeta
|> where([pm], pm.meta_key == ^meta_key)
|> where([pm], pm.post_id == ^post_id)
|> Repo.one()
|> case do
nil ->
multi
|> Multi.insert(
step_name,
Postmeta.changeset(
%Postmeta{},
%{
post_id: post_id,
meta_key: meta_key,
meta_value: new_value
}
)
)
%{meta_value: ^cast_value} ->
# No change needed
multi
meta ->
multi
|> Multi.update(step_name, Postmeta.changeset(meta, %{meta_value: new_value}))
end
end
defp update_order(multi, post_id, new_order) do
step_name = "#{post_id}.update_order"
multi
|> Multi.update_all(step_name, from(p in Post, where: p."ID" == ^post_id), [set: [menu_order: new_order]])
end
defp recursive_post_ids(params) do
params
|> Enum.flat_map(& [&1["post_id"]|recursive_post_ids(&1["children"])])
end
end

View file

@ -0,0 +1,54 @@
defmodule Content.Comment do
@moduledoc """
A comment on the site.
"""
use Ecto.Schema
import Ecto.Changeset
alias Content.{Post}
@primary_key {:comment_ID, :id, autogenerate: true}
@derive {Phoenix.Param, key: :comment_ID}
schema "wp_comments" do
belongs_to :post, Post, foreign_key: :comment_post_ID, references: :ID
field :comment_author, :string
field :comment_author_email, :string
field :comment_author_url, :string
field :comment_author_IP, :string
field :comment_date, :naive_datetime
field :comment_date_gmt, :naive_datetime
field :comment_content, :string
field :comment_karma, :integer
field :comment_approved, :string
field :comment_agent, :string
field :comment_type, :string
field :comment_parent, :integer, default: 0
field :user_id, :integer
end
def changeset(struct, params \\ %{}) do
struct
|> Map.merge(%{
comment_date: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second),
comment_date_gmt: NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second),
comment_approved: "1"
})
|> cast(params, [
:comment_ID,
:comment_post_ID,
:comment_author,
:comment_author_email,
:comment_author_url,
:comment_author_IP,
:comment_date,
:comment_date_gmt,
:comment_content,
:comment_karma,
:comment_approved,
:comment_agent,
:comment_type,
:comment_parent,
:user_id
])
|> validate_required([:comment_content])
end
end

View file

@ -0,0 +1,19 @@
defmodule Content.Commentmeta do
@moduledoc """
A piece of metadata about a comment on the site.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:meta_id, :id, autogenerate: true}
schema "wp_commentmeta" do
field :comment_id, :integer
field :meta_key, :string
field :meta_value, :string
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:meta_id, :comment_id, :meta_key, :meta_value])
end
end

View file

@ -0,0 +1,107 @@
defmodule Content.Comments do
@moduledoc """
Functions for presenting comments on the site.
"""
import Ecto.Query, warn: false
alias Content.Comment
alias Content.Repo
def children(parent_comment_id, array_of_comments) do
array_of_comments
|> Enum.filter(&(&1.comment_parent == parent_comment_id))
end
@doc """
Returns the list of comments.
## Examples
iex> list_comments()
[%Comment{}, ...]
"""
def list_comments do
Repo.all(Comment)
end
@doc """
Gets a single comment.
Raises `Ecto.NoResultsError` if the comment does not exist.
## Examples
iex> get_comment!(123)
%Comment{}
iex> get_comment!(456)
** (Ecto.NoResultsError)
"""
def get_comment!(id), do: Repo.get!(Comment, id)
@doc """
Creates a comment.
## Examples
iex> create_comment(%{field: value})
{:ok, %Comment{}}
iex> create_comment(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_comment(attrs \\ %{}) do
%Comment{}
|> Comment.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a comment.
## Examples
iex> update_comment(comment, %{field: new_value})
{:ok, %Comment{}}
iex> update_comment(comment, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_comment(%Comment{} = comment, attrs) do
comment
|> Comment.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Comment.
## Examples
iex> delete_comment(comment)
{:ok, %Comment{}}
iex> delete_comment(comment)
{:error, %Ecto.Changeset{}}
"""
def delete_comment(%Comment{} = comment) do
Repo.delete(comment)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking comment changes.
## Examples
iex> change_comment(comment)
%Ecto.Changeset{source: %Comment{}}
"""
def change_comment(%Comment{} = comment) do
Comment.changeset(comment, %{})
end
end

42
apps/content/lib/link.ex Normal file
View file

@ -0,0 +1,42 @@
defmodule Content.Link do
@moduledoc """
A link for the (deprecated) link roll feature.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:link_id, :id, autogenerate: true}
schema "wp_links" do
field :link_url, :string
field :link_name, :string
field :link_image, :string
field :link_target, :string
field :link_description, :string
field :link_visible, :string
field :link_owner, :integer
field :link_rating, :integer
field :link_updated, :naive_datetime
field :link_rel, :string
field :link_notes, :string
field :link_rss, :string
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [
:link_id,
:link_url,
:link_name,
:link_image,
:link_target,
:link_description,
:link_visible,
:link_owner,
:link_rating,
:link_updated,
:link_rel,
:link_notes,
:link_rss
])
end
end

167
apps/content/lib/menu.ex Normal file
View file

@ -0,0 +1,167 @@
defmodule Content.Menu do
@moduledoc """
Module for retrieving, manipulating, and processing navigation menus.
"""
alias Content.{Option, Post, Repo, Term, TermRelationship}
import Ecto.Query
def put_menu_option(option_name, location_name, menu_id) do
option =
Option
|> where(option_name: ^option_name)
|> Repo.one()
|> Kernel.||(%Option{option_name: option_name, option_value: "a:0:{}"})
value =
option
|> Option.parse_option_value
nav_menu_locations =
value
|> Enum.find(fn {key, _value} -> key == "nav_menu_locations" end)
|> Kernel.||({"nav_menu_locations", []})
|> elem(1)
new_nav_menu_locations =
nav_menu_locations
|> Enum.filter(fn {key, _value} -> key != location_name end)
|> Kernel.++([{location_name, menu_id}])
new_value =
value
|> Enum.filter(fn {key, _value} -> key != "nav_menu_locations" end)
|> Kernel.++([{"nav_menu_locations", new_nav_menu_locations}])
option
|> Option.put_new_value(new_value)
end
def get_menu_from_option_and_location(option_name, location_name) do
option =
Option
|> where(option_name: ^option_name)
|> Repo.one()
|> Kernel.||(%Option{option_name: option_name, option_value: "a:0:{}"})
value =
option
|> Option.parse_option_value
menu_pair =
value
|> Enum.find(fn {key, _value} -> key == "nav_menu_locations" end)
|> Kernel.||({"nav_menu_locations", []})
|> elem(1)
|> Enum.find(fn {key, _value} -> key == location_name end)
case menu_pair do
{^location_name, menu_id} ->
menu_id |> get_menu_from_id()
nil ->
nil
end
end
def get_menu_from_id(menu_id) do
menu_id
|> nav_menu_items_for_id()
|> arrange_menu_item_posts()
end
def nav_menu_items_for_id(menu_id) do
Post
|> join(
:inner,
[p],
tr in TermRelationship,
on: p."ID" == tr.object_id
)
|> order_by(:menu_order)
|> preload(:metas)
|> where([p, tr], tr.term_taxonomy_id == ^menu_id)
|> Repo.all()
end
defp arrange_menu_item_posts(nav_posts, parent_id \\ "0", nav_to_post_map \\ nil) do
nav_to_post_map = nav_to_post_map || make_nav_to_post_map(nav_posts)
nav_posts
|> Enum.filter(fn post ->
meta_map = post |> Post.metas_map
meta_map["_menu_item_menu_item_parent"] == parent_id
end)
|> Enum.map(fn post ->
meta_map = post |> Post.metas_map
related_item =
if meta_map["_menu_item_object"] == "category" do
item = nav_to_post_map["category/#{meta_map["_menu_item_object_id"]}"] || %Term{}
%{
title: item.name,
slug: item.slug,
resource: "category",
}
else
item = nav_to_post_map["post/#{meta_map["_menu_item_object_id"]}"] || %Post{}
%{
title: item.post_title,
slug: item.post_name,
resource: "posts",
}
end
%{
post_id: post."ID",
type: meta_map["_menu_item_object"],
target_id: meta_map["_menu_item_object_id"],
parent_id: meta_map["_menu_item_menu_item_parent"],
url: meta_map["_menu_item_url"],
related_item: related_item,
children: arrange_menu_item_posts(nav_posts, Integer.to_string(post."ID"), nav_to_post_map),
}
end)
end
defp make_nav_to_post_map(nav_posts) do
nav_post_meta_map = nav_posts |> Post.metas_map()
linked_post_ids =
nav_post_meta_map
|> Enum.filter(fn {_key, value} ->
value["_menu_item_object"] != "category"
end)
|> Enum.map(fn {_key, value} ->
value["_menu_item_object_id"]
end)
nav_to_post_map =
Post
|> where([p], p."ID" in ^linked_post_ids)
|> Repo.all()
|> Enum.map(fn post ->
{"post/#{post."ID"}", post}
end)
|> Map.new
linked_category_ids =
nav_post_meta_map
|> Enum.filter(fn {_key, value} ->
value["_menu_item_object"] == "category"
end)
|> Enum.map(fn {_key, value} ->
value["_menu_item_object_id"]
end)
nav_to_category_map =
Term
|> where([t], t.term_id in ^linked_category_ids)
|> Repo.all()
|> Enum.map(fn category ->
{"category/#{category.term_id}", category}
end)
|> Map.new
nav_to_post_map |> Map.merge(nav_to_category_map)
end
end

View file

@ -0,0 +1,31 @@
defmodule Content.Option do
@moduledoc """
A configuration option for the site.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:option_id, :id, autogenerate: true}
schema "wp_options" do
field :option_name, :string
field :autoload, :string
field :option_value, :string
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:option_id, :option_name, :option_value, :autoload])
end
def parse_option_value(struct) do
case PhpSerializer.unserialize(struct.option_value) do
{:ok, values} ->
values
end
end
def put_new_value(struct, value) do
struct
|> change(%{option_value: PhpSerializer.serialize(value)})
end
end

View file

@ -0,0 +1,29 @@
defmodule Content.Options do
@moduledoc """
Query the option key-value pairs for the site.
"""
alias Content.Option
alias Content.Repo
def get(key), do: Option |> Repo.get_by(option_name: key)
def get_value(key) do
case get(key) do
nil ->
nil
opt ->
opt
|> (&(&1.option_value)).()
end
end
def get_value_as_int(key) do
case get_value(key) do
nil ->
{nil, nil}
opt ->
opt
|> Integer.parse()
end
end
end

136
apps/content/lib/post.ex Normal file
View file

@ -0,0 +1,136 @@
defmodule Content.Post do
@moduledoc """
One "post" i.e. a blog post, page, attachment, or item of a custom post type.
"""
use Ecto.Schema
import Ecto.Changeset
alias Content.Slugs
@primary_key {:ID, :id, autogenerate: true}
@derive {Phoenix.Param, key: :post_name}
schema "wp_posts" do
field :post_date, :naive_datetime
field :post_date_gmt, :naive_datetime
field :post_content, :string, default: ""
field :post_title, :string
field :post_excerpt, :string
field :post_status, :string
field :comment_status, :string
field :ping_status, :string
field :post_password, :string, default: ""
field :post_name, :string
field :to_ping, :string, default: ""
field :pinged, :string, default: ""
field :post_modified, :naive_datetime
field :post_modified_gmt, :naive_datetime
field :post_content_filtered, :string, default: ""
field :post_parent, :integer
field :guid, :string
field :menu_order, :integer
field :post_type, :string
field :post_mime_type, :string
field :comment_count, :integer
field :sticky, :boolean, [virtual: true, default: false]
has_many :metas, Content.Postmeta, foreign_key: :post_id
has_many :comments, Content.Comment, foreign_key: :comment_post_ID
has_many :term_relationships, Content.TermRelationship, foreign_key: :object_id
has_many :categories, through: [:term_relationships, :category, :term]
has_many :tags, through: [:term_relationships, :tag, :term]
has_one :post_format, through: [:term_relationships, :post_format, :term]
belongs_to :author, Content.User, foreign_key: :post_author, references: :ID
end
def changeset(struct, params \\ %{}) do
struct
|> cast(
params,
[
:ID,
:post_author,
:post_date,
:post_date_gmt,
:post_content,
:post_title,
:post_excerpt,
:post_status,
:comment_status,
:ping_status,
:post_password,
:post_name,
:to_ping,
:pinged,
:post_content_filtered,
:post_parent,
:menu_order,
:post_type,
:post_mime_type,
:comment_count,
:sticky,
])
|> put_default(:post_date, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> put_default(:post_date_gmt, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> put_change(:post_modified, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> put_change(:post_modified_gmt, NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> Slugs.ensure_post_has_slug()
|> maybe_put_guid()
|> validate_required([:post_name, :post_status])
|> validate_inclusion(:post_status, ["publish", "future", "draft", "pending", "private", "trash", "auto-draft", "inherit"])
end
def put_default(changeset, key, value) do
if is_nil(changeset |> get_field(key)) do
changeset
|> put_change(key, value)
else
changeset
end
end
def before_more(string) do
string
|> String.split("<!--more-->")
|> Enum.at(0)
end
def content_page(struct, page) do
(struct.post_content || "")
|> String.split("<!--nextpage-->")
|> Enum.at(page - 1)
|> Kernel.||("")
end
def content_page_count(struct) do
(struct.post_content || "")
|> String.split("<!--nextpage-->")
|> Enum.count
end
def paginated_post?(struct) do
content_page_count(struct) > 1
end
def metas_map(list) when is_list(list) do
list
|> Enum.map(fn post ->
{post."ID", metas_map(post)}
end)
|> Map.new
end
def metas_map(%Content.Post{} = struct) do
struct.metas
|> Enum.map(&({&1.meta_key, &1.meta_value}))
|> Map.new
end
def maybe_put_guid(changeset) do
import ContentWeb.Router.Helpers, only: [posts_url: 3]
slug = changeset |> get_field(:post_name)
case slug do
nil -> changeset
_ ->
changeset
|> put_default(:guid, posts_url(ContentWeb.Endpoint, :show, slug))
end
end
end

View file

@ -0,0 +1,19 @@
defmodule Content.Postmeta do
@moduledoc """
An item of metadata about a post.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:meta_id, :id, autogenerate: true}
schema "wp_postmeta" do
belongs_to :post, Content.Post, references: :ID
field :meta_key, :string
field :meta_value, :string
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:meta_id, :post_id, :meta_key, :meta_value])
end
end

286
apps/content/lib/posts.ex Normal file
View file

@ -0,0 +1,286 @@
defmodule Content.Posts do
@page_size 3
@moduledoc """
The Content context.
"""
import Ecto.Query, warn: false
alias Content.Repo
alias Content.Option
alias Content.Post
alias Ecto.Changeset
@preloads [:metas, :author, :categories, :tags, :comments, :post_format]
@doc """
Returns the lisdpt of posts for admin interface.
## Examples
iex> list_admin_posts()
[%Post{}, ...]
"""
def list_admin_posts(page, post_type \\ "post") do
post_type = post_type || "post"
Repo.all(
from p in Post,
where: p.post_type == ^post_type,
where: p.post_status in ["publish", "future", "draft", "pending", "private", "inherit"],
preload: ^@preloads,
order_by: [desc: p.post_date],
limit: @page_size,
offset: ^(@page_size * (String.to_integer(page) - 1))
)
end
@doc """
Returns the list of posts.
## Examples
iex> list_posts()
[%Post{}, ...]
"""
def list_posts(params \\ %{}) do
page = params |> Map.get("page", "1")
normal_posts =
params
|> post_scope_for_params()
|> limit(@page_size)
|> offset(^(@page_size * (String.to_integer(page) - 1)))
|> Repo.all()
sticky_posts_for_page(params) ++ normal_posts
end
def post_scope_for_params(params) do
post_type = params |> Map.get("post_type", "post")
category = params |> Map.get("category")
query =
post_scope()
|> where([p], p.post_type == ^post_type)
if category do
query |> join(:inner, [p], term in assoc(p, :categories), on: term.slug == ^category)
else
query
end
end
def post_scope do
from p in Post,
where: p.post_status == "publish",
preload: ^@preloads,
order_by: [desc: p.post_date]
end
def post_scope_with_drafts do
from p in Post,
preload: ^@preloads,
order_by: [desc: p.post_date]
end
def sticky_posts_for_page(%{"page" => "1"} = params) do
sticky_posts =
params
|> post_scope_for_params()
|> where([p], p.'ID' in ^sticky_ids())
|> Repo.all()
sticky_posts
|> Enum.map(fn post ->
post
|> Changeset.change(%{sticky: true})
|> Changeset.apply_changes()
end)
end
def sticky_posts_for_page(_), do: []
defp sticky_ids do
case Repo.one(from opt in Option, where: opt.option_name == "sticky_posts") do
nil ->
[]
option ->
option
|> Option.parse_option_value
|> Enum.map(&(elem(&1, 1)))
end
end
def last_page(params \\ %{}) do
post_count =
params
|> post_scope_for_params()
|> Repo.aggregate(:count, :ID)
post_count
|> (&(&1 / @page_size)).()
|> Float.ceil
|> trunc
end
def thumbs_for_posts(posts) do
post_to_thumbnail_id =
posts
|> Enum.map(fn post -> {post.'ID', (post |> Post.metas_map)["_thumbnail_id"]} end)
|> Enum.reject(&(elem(&1, 1) == nil))
thumbs =
Post
|> preload(:metas)
|> where([thumb], thumb.'ID' in ^Enum.map(post_to_thumbnail_id, &(elem(&1, 1))))
|> Repo.all()
|> Enum.map(fn thumb -> {thumb.'ID', thumb} end)
|> Map.new
post_to_thumbnail_id
|> Enum.map(fn {key, value} -> {key, thumbs[String.to_integer(value)]} end)
|> Map.new
end
@doc """
Gets a single posts.
Raises `Ecto.NoResultsError` if the Wp posts does not exist.
## Examples
iex> get_posts!(123)
%Post{}
iex> get_posts!(456)
** (Ecto.NoResultsError)
"""
def get_posts!(slug) do
id_filter = fn scope, id ->
case Integer.parse(id, 10) do
:error ->
scope |> where([p], p.post_name == ^id)
{int_id, _} ->
scope |> where([p], p.'ID' == ^int_id)
end
end
post_scope()
|> where([p], p.post_type != "nav_menu_item")
|> id_filter.(slug)
|> Repo.one!()
end
@doc """
Gets a single post that may or may not be in draft status.
Raises `Ecto.NoResultsError` if the Wp posts does not exist.
## Examples
iex> get_post_with_drafts!(123)
%Post{}
iex> get_post_with_drafts!(456)
** (Ecto.NoResultsError)
"""
def get_post_with_drafts!(slug) do
id_filter = fn scope, id ->
case Integer.parse(id, 10) do
:error ->
scope |> where([p], p.post_name == ^id)
{int_id, ""} ->
scope |> where([p], p.'ID' == ^int_id)
{_int_id, _} ->
scope |> where([p], p.post_name == ^id)
end
end
post_scope_with_drafts()
|> where([p], p.post_type != "nav_menu_item")
|> id_filter.(slug)
|> Repo.one!()
end
@doc """
Creates a posts.
## Examples
iex> create_posts(%{field: value})
{:ok, %Post{}}
iex> create_posts(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_posts(attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
end
@doc """
Builds a post for preview, but does not save it.
"""
def preview_post(attrs \\ %{}) do
%Post{}
|> Repo.preload(@preloads)
|> Post.changeset(attrs)
|> Changeset.put_change(:post_name, "preview")
|> Changeset.apply_changes()
end
@doc """
Updates a posts.
## Examples
iex> update_posts(posts, %{field: new_value})
{:ok, %Post{}}
iex> update_posts(posts, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_posts(%Post{} = posts, attrs) do
posts
|> Post.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Post.
## Examples
iex> delete_posts(posts)
{:ok, %Post{}}
iex> delete_posts(posts)
{:error, %Ecto.Changeset{}}
"""
def delete_posts(%Post{} = posts) do
Repo.delete(posts)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking posts changes.
## Examples
iex> change_posts(posts)
%Ecto.Changeset{source: %Post{}}
"""
def change_posts(%Post{} = posts) do
Post.changeset(posts, %{})
end
end

5
apps/content/lib/repo.ex Normal file
View file

@ -0,0 +1,5 @@
defmodule Content.Repo do
use Ecto.Repo,
otp_app: :content,
adapter: Ecto.Adapters.Postgres
end

View file

@ -0,0 +1,9 @@
defmodule Content.Scheduler do
@moduledoc """
The Quantum cron-like scheduler for this application. See config.exs for
configured jobs.
"""
use Quantum.Scheduler,
otp_app: :content
end

View file

@ -0,0 +1,97 @@
defmodule Content.ShortcodeParser do
use Neotomex.ExGrammar
@root true
define :document, "(comment / shortcode / notcode)*" do
tail ->
tail |> Enum.join
end
define :notcode, "not_open_bracket+" do
content ->
content |> Enum.join
end
define :comment, "<open_bracket> <open_bracket> ([^\\]]+ close_bracket?)+ <close_bracket>" do
[inner] ->
inner
|> Enum.map(fn [chars, nil] -> "#{chars |> Enum.join}]" end)
|> Enum.join
|> (&("[#{&1}")).()
end
define :shortcode, "<open_bracket> <spaces?> name <spaces?> attribute* <close_bracket> <!'('> (notcode? <open_bracket> <'/'> <spaces?> name <spaces?> <close_bracket>)?" do
[name, attributes, nil] ->
Content.Shortcodes.dispatch(name, attributes)
[name, attributes, [content, closing_name]] when closing_name == name ->
Content.Shortcodes.dispatch(name, attributes, content || "")
end
define :attribute, "name <spaces?>"
define :name, "(namechar+)", do: (chars -> Enum.join(chars))
define :namechar, "[A-Za-z0-9] / dash / underscore"
define :dash, "<'-'>"
define :underscore, "<'_'>"
define :open_bracket, "<'['>", do: ["["]
define :not_open_bracket, "[^\\[]"
define :close_bracket, "<']'>", do: ["]"]
define :close_comment, "close_bracket close_bracket"
define :spaces, "[\s\\r\\n]*"
@_neotomex_definitions Map.put(@_neotomex_definitions,
:not_open_bracket,
{{:terminal, ~r/^[^[]/u}, nil})
end
defmodule Content.Shortcodes do
@moduledoc """
For handling wordpress style shortcodes in strings.
"""
def expand_shortcodes(frag) do
{:ok, tree} = Floki.parse_fragment(frag)
case tree do
[text] when is_binary(text) ->
{:ok, result} = processed_text(text) |> Floki.parse_fragment
result
_ ->
tree
|> Floki.traverse_and_update(fn
tag ->
tag |> transform_text_nodes
end)
end
|> Floki.raw_html(encode: false)
end
defp transform_text_nodes({tag_name, attrs, children}) do
new_children =
children
|> Enum.map(fn
text when is_binary(text) ->
{:ok, [result]} = processed_text(text) |> Floki.parse_fragment
result
other -> other
end)
{tag_name, attrs, new_children}
end
defp processed_text(text) do
text =
text
|> String.replace("\r", "")
case Content.ShortcodeParser.parse(text) do
{:ok, result, remainder} ->
[result, remainder] |> Enum.join
{:ok, result} ->
result
end
end
def dispatch(tag, _attrs), do: String.upcase(String.reverse(tag))
def dispatch(_tag, _attrs, content) do
String.upcase(String.reverse(content))
end
end

68
apps/content/lib/slugs.ex Normal file
View file

@ -0,0 +1,68 @@
defmodule Content.Slugs do
@moduledoc """
Provides functions for working with post slugs and ensuring that they are unique.
"""
import Ecto.{Changeset, Query}
alias Content.{Post, Repo}
def ensure_post_has_slug(changeset) do
cond do
!is_nil(changeset |> get_field(:post_name)) ->
changeset
is_nil(changeset |> get_field(:post_title)) ->
changeset
|> put_change(
:post_name,
changeset
|> get_field(:post_date)
|> Kernel.||(NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second))
|> Timex.format!("%F", :strftime)
|> Slugger.slugify_downcase()
|> unique_slug(changeset |> get_field(:ID))
)
true ->
changeset
|> put_change(
:post_name,
changeset
|> get_field(:post_title)
|> Slugger.slugify_downcase()
|> unique_slug(changeset |> get_field(:ID))
)
end
end
defp unique_slug(proposed_slug, post_id, postfix_number \\ 0) do
proposed_slug_with_postfix =
if postfix_number == 0 do
proposed_slug
else
"#{proposed_slug}-#{postfix_number}"
end
competition_count =
Repo.aggregate(
(
Post
|> where([post], post.post_name == ^proposed_slug_with_postfix)
|> post_id_match(post_id)
),
:count,
:ID
)
if competition_count == 0 do
proposed_slug_with_postfix
else
unique_slug(proposed_slug, post_id, postfix_number + 1)
end
end
defp post_id_match(query, nil) do
query
end
defp post_id_match(query, id) when is_number(id) do
from p in query, where: p.'ID' != ^id
end
end

19
apps/content/lib/term.ex Normal file
View file

@ -0,0 +1,19 @@
defmodule Content.Term do
@moduledoc """
Represents one 'term', i.e. a grouping under a taxonomy.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:term_id, :id, autogenerate: true}
schema "wp_terms" do
field :name, :string
field :slug, :string
field :term_group, :integer
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:term_id, :name, :slug, :term_group])
end
end

View file

@ -0,0 +1,43 @@
defmodule Content.TermRelationship do
@moduledoc """
Maintains the relationship between a term_taxonomy and a post / page / or object.
"""
use Ecto.Schema
import Ecto.Changeset
alias Content.{Post}
@primary_key {:object_id, :integer, []}
@primary_key {:term_taxonomy_id, :integer, []}
schema "wp_term_relationships" do
field :term_order, :integer
belongs_to :post, Post, foreign_key: :object_id, references: :ID
belongs_to :term_taxonomy,
Content.TermTaxonomy,
foreign_key: :term_taxonomy_id,
references: :term_taxonomy_id,
define_field: false
belongs_to :category,
Content.TermTaxonomy,
foreign_key: :term_taxonomy_id,
references: :term_taxonomy_id,
define_field: false,
where: [taxonomy: "category"]
belongs_to :tag,
Content.TermTaxonomy,
foreign_key: :term_taxonomy_id,
references: :term_taxonomy_id,
define_field: false,
where: [taxonomy: "post_tag"]
belongs_to :post_format,
Content.TermTaxonomy,
foreign_key: :term_taxonomy_id,
references: :term_taxonomy_id,
define_field: false,
where: [taxonomy: "post_format"]
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:object_id, :term_taxonomy_id, :term_order])
end
end

View file

@ -0,0 +1,21 @@
defmodule Content.TermTaxonomy do
@moduledoc """
A record in a taxonomy which organizes terms and posts in the system.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:term_taxonomy_id, :id, autogenerate: true}
schema "wp_term_taxonomy" do
field :taxonomy, :string
field :description, :string
field :parent, :integer
field :count, :integer
belongs_to :term, Content.Term, foreign_key: :term_id, references: :term_id
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:term_taxonomy_id, :term_id, :taxonomy, :description, :parent, :count])
end
end

View file

@ -0,0 +1,19 @@
defmodule Content.Termmeta do
@moduledoc """
Represents one piece of metadata around one "term" (a grouping under a taxonomy).
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:meta_id, :id, autogenerate: true}
schema "wp_termmeta" do
field :term_id, :integer
field :meta_key, :string
field :meta_value, :string
end
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:meta_id, :term_id, :meta_key, :meta_value])
end
end

15
apps/content/lib/terms.ex Normal file
View file

@ -0,0 +1,15 @@
defmodule Content.Terms do
@moduledoc """
This module contains functions for retrieving, manipulating, and saving
Terms.
"""
import Ecto.Query
def categories do
from t in Content.Term,
join: tt in Content.TermTaxonomy,
on: t.term_id == tt.term_id,
where: tt.taxonomy == "category"
end
end

84
apps/content/mix.exs Normal file
View file

@ -0,0 +1,84 @@
defmodule Content.Mixfile do
use Mix.Project
def project do
[
app: :content,
version: "0.0.1",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
start_permanent: Mix.env == :prod,
aliases: aliases(),
deps: deps(),
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test]
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {Content.Application, []},
extra_applications: [:logger, :runtime_tools, :sitemap]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:content_web, in_umbrella: true},
{:phoenix, "~> 1.5.3"},
{:phoenix_pubsub, "~> 2.0"},
{:ecto_sql, "~> 3.4"},
{:postgrex, "~> 0.15.0"},
{:plug_cowboy, "~> 2.0"},
{:phoenix_ecto, "~> 4.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:gettext, "~> 0.11"},
{:cowboy, "~> 2.7"},
{:php_serializer, "~> 0.9.0"},
{:quantum, "~> 2.3"},
{:timex, "~> 3.1"},
{:excoveralls, "~> 0.10", only: [:dev, :test]},
{:phoenix_html_sanitizer, "~> 1.0.0"},
{:bcrypt_elixir, "~> 1.0"},
{:comeonin, "~> 4.0"},
{:earmark, "~> 1.4.2" },
{:slugger, "~> 0.3"},
{:ecto, "~> 3.4.3"},
{:floki, "~> 0.25.0"},
{:mock, "~> 0.3.0", only: :test},
{:meck, "~> 0.8.13", only: :test},
{:sitemap, "~> 1.1"},
{:neotomex, "~> 0.1.7"},
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end
end

View file

@ -0,0 +1,97 @@
## `msgid`s in this file come from POT (.pot) files.
##
## Do not add, change, or remove `msgid`s manually here as
## they're tied to the ones in the corresponding POT file
## (with the same domain).
##
## Use `mix gettext.extract --merge` or `mix gettext.merge`
## to merge POT files into PO files.
msgid ""
msgstr ""
"Language: en\n"
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View file

@ -0,0 +1,95 @@
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
## From Ecto.Changeset.cast/4
msgid "can't be blank"
msgstr ""
## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken"
msgstr ""
## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""
## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted"
msgstr ""
## From Ecto.Changeset.validate_format/3
msgid "has invalid format"
msgstr ""
## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry"
msgstr ""
## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved"
msgstr ""
## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation"
msgstr ""
## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry"
msgstr ""
msgid "are still associated with this entry"
msgstr ""
## From Ecto.Changeset.validate_length/3
msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)"
msgstr[0] ""
msgstr[1] ""
msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)"
msgstr[0] ""
msgstr[1] ""
## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}"
msgstr ""
msgid "must be greater than %{number}"
msgstr ""
msgid "must be less than or equal to %{number}"
msgstr ""
msgid "must be greater than or equal to %{number}"
msgstr ""
msgid "must be equal to %{number}"
msgstr ""

View file

@ -0,0 +1,133 @@
defmodule Content.Repo.Migrations.CreateWpSchema do
use Ecto.Migration
def change do
create table("wp_commentmeta", primary_key: false) do
add :meta_id, :serial, primary_key: true
add :comment_id, :integer
add :meta_key, :text
end
create table("wp_comments", primary_key: false) do
add :comment_ID, :serial, primary_key: true
add :comment_post_ID, :integer
add :comment_author, :text
add :comment_author_email, :text
add :comment_author_url, :text
add :comment_author_IP, :text
add :comment_date, :naive_datetime
add :comment_date_gmt, :naive_datetime
add :comment_content, :string
add :comment_karma, :integer
add :comment_approved, :text
add :comment_agent, :text
add :comment_type, :text
add :comment_parent, :integer
add :user_id, :integer
end
create table("wp_links", primary_key: false) do
add :link_id, :serial, primary_key: true
add :link_url, :text
add :link_name, :text
add :link_image, :text
add :link_target, :text
add :link_description, :text
add :link_visible, :text
add :link_owner, :integer
add :link_rating, :integer
add :link_updated, :naive_datetime
add :link_rel, :text
add :link_rss, :text
end
create table("wp_options", primary_key: false) do
add :option_id, :serial, primary_key: true
add :option_name, :text
add :autoload, :text
add :option_value, :text
end
create table("wp_postmeta", primary_key: false) do
add :meta_id, :serial, primary_key: true
add :post_id, :integer
add :meta_key, :text
add :meta_value, :text
end
create table("wp_posts", primary_key: false) do
add :ID, :integer, [:primary_key]
add :post_author, :integer
add :post_date, :naive_datetime
add :post_date_gmt, :naive_datetime
add :post_content, :text
add :post_title, :string
add :post_excerpt, :string
add :post_status, :text
add :comment_status, :text
add :ping_status, :text
add :post_password, :text
add :post_name, :text
add :to_ping, :string
add :pinged, :string
add :post_modified, :naive_datetime
add :post_modified_gmt, :naive_datetime
add :post_content_filtered, :text
add :post_parent, :integer
add :guid, :text
add :menu_order, :integer
add :post_type, :text
add :post_mime_type, :text
add :comment_count, :integer
end
create table("wp_term_relationships", primary_key: false) do
add :object_id, :serial, primary_key: true
add :term_taxonomy_id, :integer, [:primary_key]
add :term_order, :integer
end
create table("wp_term_taxonomy", primary_key: false) do
add :term_taxonomy_id, :serial, primary_key: true
add :term_id, :integer
add :taxonomy, :text
add :description, :text
add :parent, :integer
add :count, :integer
end
create table("wp_termmeta", primary_key: false) do
add :meta_id, :serial, primary_key: true
add :term_id, :integer
add :meta_key, :text
add :meta_value, :text
end
create table("wp_terms", primary_key: false) do
add :term_id, :serial, primary_key: true
add :name, :text
add :slug, :text
add :term_group, :integer
end
create table("wp_usermeta", primary_key: false) do
add :umeta_id, :serial, primary_key: true
add :user_id, :integer
add :meta_key, :text
add :meta_value, :text
end
create table("wp_users", primary_key: false) do
add :ID, :integer, [:primary_key]
add :user_login, :text
add :user_pass, :text
add :user_nicename, :text
add :user_email, :text
add :user_url, :text
add :user_registered, :naive_datetime
add :user_activation_key, :text
add :user_status, :integer
add :display_name, :text
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Content.Repo.Migrations.ChangeCommentContentToText do
use Ecto.Migration
def change do
alter table("wp_comments") do
modify :comment_content, :text
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Content.Repo.Migrations.AddDefaultToCommentCount do
use Ecto.Migration
def change do
alter table("wp_posts") do
modify :comment_count, :integer, default: 0
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Content.Repo.Migrations.AddMetaValueToWpCommentmetas do
use Ecto.Migration
def change do
alter table("wp_commentmeta") do
add :meta_value, :string
end
end
end

View file

@ -0,0 +1,9 @@
defmodule Content.Repo.Migrations.AddLinkNotesToWpLinks do
use Ecto.Migration
def change do
alter table("wp_links") do
add :link_notes, :string
end
end
end

View file

@ -0,0 +1,7 @@
defmodule Content.Repo.Migrations.AddUniqueIndexToPostName do
use Ecto.Migration
def change do
create unique_index(:wp_posts, ["post_name"])
end
end

View file

@ -0,0 +1,11 @@
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# Content.Repo.insert!(%Content.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

View file

@ -0,0 +1,69 @@
defmodule Content.AttachmentTest do
use Content.DataCase
alias Content.{Attachment, Postmeta, Posts, Repo}
@create_attrs %{
ID: 123,
post_name: "my-attachment",
post_title: "My Attachment",
post_content: "",
post_status: "publish",
post_type: "attachment",
post_date: "2018-01-01T00:00:00Z"
}
def fixture(:wide_attachment) do
{:ok, attachment} = Posts.create_posts(@create_attrs)
{:ok, _meta} =
%Postmeta{
post_id: attachment."ID",
meta_key: "_wp_attachment_metadata",
meta_value: "a:2:{s:5:\"width\";i:640;s:6:\"height\";i:480;}"
} |> Repo.insert()
Content.Post
|> preload([:metas])
|> Repo.get!(attachment."ID")
end
def fixture(:tall_attachment) do
{:ok, attachment} = Posts.create_posts(@create_attrs)
{:ok, _meta} =
%Postmeta{
post_id: attachment."ID",
meta_key: "_wp_attachment_metadata",
meta_value: "a:2:{s:5:\"width\";i:480;s:6:\"height\";i:640;}"
} |> Repo.insert()
Content.Post
|> preload([:metas])
|> Repo.get!(attachment."ID")
end
def fixture(:unknown_dimensions) do
{:ok, attachment} = Posts.create_posts(@create_attrs)
Content.Post
|> preload([:metas])
|> Repo.get!(attachment."ID")
end
describe "dimensions" do
test "can get dimensions" do
assert Attachment.dimensions(fixture(:wide_attachment)) == %{width: 640, height: 480}
end
test "returns nil if dimensions are missing" do
assert is_nil(Attachment.dimensions(fixture(:unknown_dimensions)))
end
end
describe "vertical?" do
test "returns true if vertical image" do
assert Attachment.vertical?(fixture(:tall_attachment))
end
test "returns false if not vertical image" do
refute Attachment.vertical?(fixture(:wide_attachment))
end
end
end

View file

@ -0,0 +1,15 @@
defmodule Content.CommentmetaTest do
use Content.DataCase
alias Content.{Commentmeta, Repo}
test "can save a new commentmeta" do
%Commentmeta{}
|> Commentmeta.changeset(%{
comment_id: 123,
meta_key: "testcommentmeta",
meta_value: "some value",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,53 @@
defmodule Content.CommentsTest do
use Content.DataCase
alias Content.{Comment, Comments, Repo}
alias Ecto.Changeset
def fixture(:parent_comment) do
%Comment{
comment_ID: 123,
comment_content: "Hello world",
comment_post_ID: 456,
}
|> Repo.insert!()
end
def fixture(:child_comment) do
%Comment{
comment_ID: 456,
comment_parent: 123,
comment_content: "Hello back",
comment_post_ID: 456,
}
|> Repo.insert!()
end
describe "children" do
test "can get children of a comment that has them" do
parent = fixture(:parent_comment)
kid = fixture(:child_comment)
kids = Comments.children(parent.comment_ID, Comments.list_comments)
assert kids == [kid]
end
test "returns an empty list if the comment has no children " do
parent = fixture(:parent_comment)
kids = Comments.children(parent.comment_ID, Comments.list_comments)
assert kids == []
end
end
describe "change comment" do
test "gives a changeset" do
changeset = fixture(:parent_comment) |> Comments.change_comment
changed_value =
changeset
|> Changeset.put_change(:comment_content, "woops")
|> Changeset.get_change(:comment_content)
assert changed_value == "woops"
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Content.LinkTest do
use Content.DataCase
alias Content.{Link, Repo}
test "can save a new link" do
%Link{}
|> Link.changeset(%{
link_url: "https://example.com"
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,141 @@
defmodule Content.MenuTest do
use Content.DataCase
alias Content.{Menu, Option, Post, Postmeta, Repo, Term, TermRelationship}
@theme_option %Option{
option_name: "test_theme",
option_value: "a:1:{s:18:\"nav_menu_locations\";a:1:{s:3:\"top\";i:13;}}"
}
@term_relationship %TermRelationship{
term_taxonomy_id: 13,
object_id: 123,
}
@top_nav_item %Post{
ID: 123,
post_name: "home",
post_title: "Home",
post_content: "",
post_status: "publish",
post_type: "nav_item",
post_date: ~N"2018-01-01T00:00:00",
comment_status: "open",
}
@top_nav_metas [
%{
post_id: 123,
meta_key: "_menu_item_object_id",
meta_value: "456",
},
%{
post_id: 123,
meta_key: "_menu_item_object",
meta_value: "post",
},
%{
post_id: 123,
meta_key: "_menu_item_menu_item_parent",
meta_value: "0",
},
]
@category_nav_metas [
%{
post_id: 123,
meta_key: "_menu_item_object_id",
meta_value: "42",
},
%{
post_id: 123,
meta_key: "_menu_item_object",
meta_value: "category",
},
%{
post_id: 123,
meta_key: "_menu_item_menu_item_parent",
meta_value: "0",
},
]
@related_page %Post {
ID: 456,
post_title: "Test Nav Home",
}
@related_category %Term{
term_id: 42,
name: "Test Category",
slug: "test-category",
}
def fixture(:option) do
@theme_option |> Repo.insert()
end
def fixture(:menu) do
{:ok, option} = fixture(:option)
{:ok, _term_relationship} = @term_relationship |> Repo.insert()
{:ok, _nav_item} = @top_nav_item |> Repo.insert()
{3, nil} = Repo.insert_all(Postmeta, @top_nav_metas)
{:ok, _post} = @related_page |> Repo.insert()
option
end
def fixture(:category_menu) do
{:ok, option} = fixture(:option)
{:ok, _term_relationship} = @term_relationship |> Repo.insert()
{:ok, _nav_item} = @top_nav_item |> Repo.insert()
{3, nil} = Repo.insert_all(Postmeta, @category_nav_metas)
{:ok, _category} = @related_category |> Repo.insert()
option
end
describe "get_menu_from_option_and_location" do
test "returns an empty if the menu is not present" do
fixture(:option)
assert Menu.get_menu_from_option_and_location("test_theme", "top") == []
end
test "returns items if the menu is present" do
fixture(:menu)
menu = Menu.get_menu_from_option_and_location("test_theme", "top")
refute menu == []
assert (menu |> Enum.at(0)) == %{
children: [],
parent_id: "0",
post_id: 123,
related_item: %{resource: "posts", slug: nil, title: "Test Nav Home"},
target_id: "456",
type: "post",
url: nil,
}
end
test "returns items if the menu has a category" do
fixture(:category_menu)
menu = Menu.get_menu_from_option_and_location("test_theme", "top")
refute menu == []
assert (menu |> Enum.at(0)) == %{
children: [],
parent_id: "0",
post_id: 123,
related_item: %{resource: "category", slug: "test-category", title: "Test Category"},
target_id: "42",
type: "category",
url: nil,
}
end
end
describe "put_menu_option" do
test "it can change the active menu in a position" do
fixture(:menu)
{:ok, _option} = Menu.put_menu_option("test_theme", "top", 7) |> Repo.update()
assert Menu.get_menu_from_option_and_location("test_theme", "top") == []
end
end
end

View file

@ -0,0 +1,14 @@
defmodule Content.OptionTest do
use Content.DataCase
alias Content.{Option, Repo}
test "can save a new link" do
%Option{}
|> Option.changeset(%{
option_name: "test_up",
option_value: "1",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,26 @@
defmodule Content.OptionsTest do
use Content.DataCase
alias Content.{Option, Options, Repo}
def fixture(:option) do
%Option{}
|> Option.changeset(%{
option_name: "test_up",
option_value: "1",
})
|> Repo.insert!()
end
test "can get an option by name" do
fixture(:option)
assert Options.get_value("test_up") == "1"
end
test "can get an option by name as an int" do
fixture(:option)
assert Options.get_value_as_int("test_up") == {1, ""}
end
end

View file

@ -0,0 +1,15 @@
defmodule Content.PostmetaTest do
use Content.DataCase
alias Content.{Postmeta, Repo}
test "can save a new postmeta" do
%Postmeta{}
|> Postmeta.changeset(%{
post_id: 123,
meta_key: "testpostmeta",
meta_value: "some value",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,71 @@
defmodule Content.ShortcodesTest do
use ExUnit.Case
import Content.Shortcodes
describe "shortcodes" do
test "no shortcodes, no problem" do
assert expand_shortcodes("test") == "test"
end
test "double bracket escapes work" do
assert expand_shortcodes("[[test]]") == "[test]"
end
test "escapes enclosing shortcodes" do
assert expand_shortcodes("[[test] abc [/test]]") == "[test] abc [/test]"
end
test "escapes shortcodes with args" do
assert expand_shortcodes("[[test with-args][/test]]") == "[test with-args][/test]"
end
test "expands shortcodes" do
assert expand_shortcodes("[test]") == "TSET"
end
test "expands shortcodes in the middle" do
assert expand_shortcodes("this is a [test] of the shortcode system") == "this is a TSET of the shortcode system"
end
test "expands shortcodes at the end" do
assert expand_shortcodes("this is a [test]") == "this is a TSET"
end
test "expands shortcodes at the beginning" do
assert expand_shortcodes("[test] it up") == "TSET it up"
end
test "handles shortcodes with args" do
assert expand_shortcodes("[test with-args]") == "TSET"
end
test "handles enclosing shortcodes" do
assert expand_shortcodes("[test]Content[/test]") == "TNETNOC"
end
test "handles enclosing shortcodes with args" do
assert expand_shortcodes("[test with-args]Content[/test]") == "TNETNOC"
end
test "handles enclosing shortcodes with no content" do
assert expand_shortcodes("[test with-args][/test]") == ""
end
test "handles strings with carriage returns" do
assert expand_shortcodes(" | \r\n ") == " | \n "
end
test "handles strings with high unicode characters" do
assert expand_shortcodes("") == ""
end
test "handles shortcodes within tags" do
assert expand_shortcodes("<p>[test]<em>chacha</em></p>") == "<p>TSET<em>chacha</em></p>"
end
test "handles mangled shortcodes gracefully" do
assert expand_shortcodes("[[unclosed shortcode") == "[[unclosed shortcode"
end
end
end

View file

@ -0,0 +1,91 @@
defmodule Content.SlugsTest do
use Content.DataCase
alias Content.{Post, Posts, Repo, Slugs}
alias Ecto.Changeset
@create_attrs %{
ID: 123,
post_name: "my-post",
post_title: "My Post",
post_content: "",
post_status: "publish",
post_type: "post",
post_date: "2018-01-01T00:00:00Z"
}
@dupe_title_attrs %{
ID: 456,
post_title: "My Post",
post_content: "",
post_status: "publish",
post_type: "post",
post_date: "2018-01-01T00:00:00Z"
}
describe "ensure_post_has_slug" do
test "doesn't overwrite a set slug" do
new_post =
%Post{
post_name: "a-set-slug"
}
|> Post.changeset()
|> Slugs.ensure_post_has_slug()
|> Changeset.apply_changes()
assert new_post.post_name == "a-set-slug"
end
test "works even if the title is nil" do
new_post =
%Post{}
|> Changeset.change(%{})
|> Slugs.ensure_post_has_slug()
|> Changeset.apply_changes()
assert new_post.post_name |> String.length() > 0
end
test "sets a slug if the title is there" do
new_post =
%Post{
post_title: "My NEW Post"
}
|> Changeset.change(%{})
|> Slugs.ensure_post_has_slug()
|> Changeset.apply_changes()
assert new_post.post_name == "my-new-post"
end
test "ensures uniqueness of the slug" do
{:ok, og_post} = Posts.create_posts(@create_attrs)
assert Post |> Repo.aggregate(:count, :ID) == 1
new_post =
%Post{
post_title: "MY POST"
}
|> Changeset.change(%{})
|> Slugs.ensure_post_has_slug()
|> Changeset.apply_changes()
assert new_post.post_name != og_post.post_name
assert new_post.post_name == "my-post-1"
end
test "ensures uniqueness of the slug on update" do
{:ok, og_post} = Posts.create_posts(@create_attrs)
assert Post |> Repo.aggregate(:count, :ID) == 1
new_post =
%Post{}
|> Changeset.change(@dupe_title_attrs)
|> Slugs.ensure_post_has_slug()
|> Changeset.apply_changes()
assert new_post.post_name != og_post.post_name
assert new_post.post_name == "my-post-1"
end
end
end

View file

@ -0,0 +1,14 @@
defmodule Content.TermRelationshipTest do
use Content.DataCase
alias Content.{Repo, TermRelationship}
test "can save a new term relationship" do
%TermRelationship{}
|> TermRelationship.changeset(%{
object_id: 123,
term_taxonomy_id: 456,
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,13 @@
defmodule Content.TermTaxonomyTest do
use Content.DataCase
alias Content.{Repo, TermTaxonomy}
test "can save a new term taxonomy" do
%TermTaxonomy{}
|> TermTaxonomy.changeset(%{
taxonomy: "post_tag",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,14 @@
defmodule Content.TermTest do
use Content.DataCase
alias Content.{Repo, Term}
test "can save a new term" do
%Term{}
|> Term.changeset(%{
slug: "testterm",
name: "Test Term",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,15 @@
defmodule Content.TermmetaTest do
use Content.DataCase
alias Content.{Repo, Termmeta}
test "can save a new termmeta" do
%Termmeta{}
|> Termmeta.changeset(%{
term_id: 123,
meta_key: "testtermmeta",
meta_value: "some value",
})
|> Repo.insert!()
end
end

View file

@ -0,0 +1,56 @@
defmodule Content.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
alias Ecto.Adapters.SQL.Sandbox
alias Ecto.Changeset
using do
quote do
alias Content.Repo
import Ecto
import Ecto.Query
import Content.DataCase
end
end
setup tags do
:ok = Sandbox.checkout(Content.Repo)
unless tags[:async] do
Sandbox.mode(Content.Repo, {:shared, self()})
end
:ok
end
@doc """
A helper that transform changeset errors to a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Changeset.traverse_errors(changeset, fn {message, opts} ->
Enum.reduce(opts, message, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end

View file

@ -0,0 +1,3 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Content.Repo, :manual)

View file

@ -16,6 +16,7 @@ defmodule ContentWeb.Router do
scope "/", ContentWeb do
pipe_through :browser
get "/posts/:id", PostsController, :show
get "/:id", PageController, :show
end
end

View file

@ -1,47 +1,73 @@
%{
"bamboo": {:hex, :bamboo, "1.5.0", "1926107d58adba6620450f254dfe8a3686637a291851fba125686fa8574842af", [:mix], [{:hackney, ">= 1.13.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d5f3d04d154e80176fd685e2531e73870d8700679f14d25a567e448abce6298d"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "10f658be786bd2daaadcd45cc5b598da01d5bbc313da4d0e3efb2d6a511d896d"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"cldr_utils": {:hex, :cldr_utils, "2.9.1", "be714403abe1a7abed5ee4f7dd3823a9067f96ab4b0613a454177b51ca204236", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6cba0a485f57feb773291ca1816469ddd887e22d73d9b12a1b207d82a67a4e71"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm", "d8700a0ca4dbb616c22c9b3f6dd539d88deaafec3efe66869d6370c9a559b3e9"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
"crontab": {:hex, :crontab, "1.1.10", "dc9bb1f4299138d47bce38341f5dcbee0aa6c205e864fba7bc847f3b5cb48241", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "1347d889d1a0eda997990876b4894359e34bfbbd688acbb0ba28a2795ca40685"},
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"earmark": {:hex, :earmark, "1.4.10", "bddce5e8ea37712a5bfb01541be8ba57d3b171d3fa4f80a0be9bcf1db417bcaf", [:mix], [{:earmark_parser, ">= 1.4.10", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "12dbfa80810478e521d3ffb941ad9fbfcbbd7debe94e1341b4c4a1b2411c1c27"},
"earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"},
"ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"},
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"ex_cldr": {:hex, :ex_cldr, "2.13.0", "742f14a4afcfea61a190d603d8e555d2c91d71e4e8fc2520d5dc35616969e225", [:mix], [{:cldr_utils, "~> 2.3", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "5e4cf3e945ee60156a3342e2a762f69036ffbe1f80520cc88592d68f12c5db55"},
"ex_prompt": {:hex, :ex_prompt, "0.1.5", "b136642d0962f8ea37b3c9fa185ad1f42c71c3b9c6c3950f0358d7f3d2db2970", [:mix], [], "hexpm", "ad19a404708c9c7b05d36090b2d074ceafbed248a8de1a22d45a05ebe6994b83"},
"excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"},
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
"floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "631f4e627c46d5ecd347df5a2accdaf0621c77c3693c5b75a8ad58e84c61f242"},
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"},
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"},
"gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
"html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.0.1", "2572e7122c78ab7e57b613e7c7f5e42bf9b3c25e430e32f23f1413d86db8a0af", [:mix], [{:mochiweb, "~> 2.12.2", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "c334e2835e094fb9c04658bd4cfc7533fa51a8f56f11343c57ab9cb2a01d8613"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
"libring": {:hex, :libring, "1.5.0", "44313eb6862f5c9168594a061e9d5f556a9819da7c6444706a9e2da533396d70", [:mix], [], "hexpm", "04e843d4fdcff49a62d8e03778d17c6cb2a03fe2d14020d3825a1761b55bd6cc"},
"linguist": {:hex, :linguist, "0.3.0", "2984dfce6720d1212ddd7bba82496f92627a39aecd4d32c7016ec00393e1f925", [:mix], [{:ex_cldr, "~> 2.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "1923876545db22b63334c9d203ef56397a2946daa018117767b068f856be41e4"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mochiweb": {:hex, :mochiweb, "2.12.2", "80804ad342afa3d7f3524040d4eed66ce74b17a555de454ac85b07c479928e46", [:make, :rebar], [], "hexpm", "d3e681d4054b74a96cf2efcd09e94157ab83a5f55ddc4ce69f90b8144673bd7a"},
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
"neotomex": {:hex, :neotomex, "0.1.7", "64f76513653aa87ea7abdde0fd600e56955d838020a13d88f2bf334c88ac3e7a", [:mix], [], "hexpm", "4b87b8f614d1cd89dc8ba80ba0e559bedb3ebf6f6d74cd774fcfdd215e861445"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
"phoenix_html_sanitizer": {:hex, :phoenix_html_sanitizer, "1.0.2", "e2c8cfbc83660e362753de127cc957bec3442a8aecdf271fb65a684a906fccf5", [:mix], [{:html_sanitize_ex, "~> 1.0.0", [hex: :html_sanitize_ex, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "47aebb08fa954b7ad95f295fb701df9800ee3a489212119c9c6074a65e1e5a10"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.6", "1b4e1b7d797386b7f9d70d2af931dc9843a5f2f2423609d22cef1eec4e4dba7d", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.13.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "b20dcad98c4ca63d38a7f5e7a40936e1e8e9da983d3d722b88ae33afb866c9ca"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.13.3", "2186c55cc7c54ca45b97c6f28cfd267d1c61b5f205f3c83533704cd991bdfdec", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "c6309a7da2e779cb9cdf2fb603d75f38f49ef324bedc7a81825998bd1744ff8a"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"php_serializer": {:hex, :php_serializer, "0.9.2", "59c5fd6bd3096671fd89358fb8229341ac7423b50ad8d45a15213b02ea2edab2", [:mix], [], "hexpm", "34eb835a460944f7fc216773b363c02e7dcf8ac0390c9e9ccdbd92b31a7ca59a"},
"plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"},
"plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"},
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
"pow": {:hex, :pow, "1.0.20", "b99993811af5233681bfc521e81ca706d25a56f2be54bad6424db327ce840ab9", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.3.0 and < 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b6bd271399ccb353abbdbdc316199fe7fd7ae36bbf47059d53e366831c34fc8"},
"quantum": {:hex, :quantum, "2.4.0", "f2ad4b20988f848455d35ed0e884ba0c7629a27ee86cbec6a6e0fc214b6e69cf", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12 or ~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: true]}], "hexpm", "a125a9e65a5af740a1198f3b05c1a736fce3942f5e0dc2901e0f9be5745bea99"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"sitemap": {:hex, :sitemap, "1.1.0", "23a019cccef7c17090d0b493354ee47a94549db64fd1cf39bda7eb41c567729c", [:mix], [{:xml_builder, ">= 0.0.0", [hex: :xml_builder, repo: "hexpm", optional: false]}], "hexpm", "d21f2c3ac65567fbdbe231f9faaf802a48405aa487d24052964d3d818a3d8c22"},
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm", "94884f84783fc1ba027aba8fe8a7dae4aad78c98e9f9c76667ec3471585c08c6"},
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"},
"telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"},
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
"xml_builder": {:hex, :xml_builder, "2.1.2", "90cb9ad382958934c78c6ddfbe6d385a8ce147d84b61cbfa83ec93a169d0feab", [:mix], [], "hexpm", "b89046041da2fbc1d51d31493ba31b9d5fc6223c93384bf513a1a9e1df9ec081"},
"yamerl": {:hex, :yamerl, "0.8.0", "8214cfe16bbabe5d1d6c14a14aea11c784b9a21903dd6a7c74f8ce180adae5c7", [:rebar3], [], "hexpm", "010634477bf9c208a0767dcca89116c2442cf0b5e87f9c870f85cd1c3e0c2aab"},
"yaml_elixir": {:hex, :yaml_elixir, "2.4.0", "2f444abc3c994c902851fde56b6a9cb82895c291c05a0490a289035c2e62ae71", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4e25a6d5c873e393689c6f1062c5ec90f6cd1be2527b073178ae37eae4c78bee"},
}