mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
topics/polls forms, relative dates
This commit is contained in:
parent
3ee6c07609
commit
d6e07b8316
17 changed files with 449 additions and 63 deletions
|
@ -1,8 +1,17 @@
|
|||
import { $, $$, clearEl, removeEl, insertBefore } from './utils/dom';
|
||||
import { delegate, leftClick } from './utils/events';
|
||||
|
||||
function pollOptionRemover(_event, target) {
|
||||
removeEl(target.closest('.js-poll-option'));
|
||||
}
|
||||
|
||||
function pollOptionCreator() {
|
||||
const addPollOptionButton = $('.js-poll-add-option');
|
||||
|
||||
delegate(document, 'click', {
|
||||
'.js-option-remove': leftClick(pollOptionRemover)
|
||||
});
|
||||
|
||||
if (!addPollOptionButton) {
|
||||
return;
|
||||
}
|
||||
|
@ -16,13 +25,10 @@ function pollOptionCreator() {
|
|||
if (existingOptionCount < maxOptionCount) {
|
||||
// The element right before the add button will always be the last field, make a copy
|
||||
const prevFieldCopy = addPollOptionButton.previousElementSibling.cloneNode(true);
|
||||
// Clear its value and increment the N in "Option N" in the placeholder attribute
|
||||
clearEl($$('.js-option-id', prevFieldCopy));
|
||||
const input = $('.js-option-label', prevFieldCopy);
|
||||
input.value = '';
|
||||
input.setAttribute('placeholder', input.getAttribute('placeholder').replace(/\d+$/, m => parseInt(m, 10) + 1));
|
||||
const newHtml = prevFieldCopy.outerHTML.replace(/(\d+)/g, `${existingOptionCount}`);
|
||||
|
||||
// Insert copy before the button
|
||||
insertBefore(addPollOptionButton, prevFieldCopy);
|
||||
addPollOptionButton.insertAdjacentHTML("beforebegin", newHtml);
|
||||
existingOptionCount++;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { $$, makeEl, findFirstTextNode } from './utils/dom';
|
||||
import { fire, delegate } from './utils/events';
|
||||
import { fire, delegate, leftClick } from './utils/events';
|
||||
|
||||
const headers = () => ({
|
||||
'x-csrf-token': window.booru.csrfToken
|
||||
|
@ -85,10 +85,6 @@ function linkRemote(event, target) {
|
|||
);
|
||||
}
|
||||
|
||||
function leftClick(func) {
|
||||
return (event, target) => { if (event.button === 0) return func(event, target); };
|
||||
}
|
||||
|
||||
delegate(document, 'click', {
|
||||
'a[data-confirm],button[data-confirm],input[data-confirm]': leftClick(confirm),
|
||||
'a[data-disable-with],button[data-disable-with],input[data-disable-with]': leftClick(disable),
|
||||
|
|
|
@ -10,6 +10,10 @@ export function on(node, event, selector, func) {
|
|||
delegate(node, event, { [selector]: func });
|
||||
}
|
||||
|
||||
export function leftClick(func) {
|
||||
return (event, target) => { if (event.button === 0) return func(event, target); };
|
||||
}
|
||||
|
||||
export function delegate(node, event, selectors) {
|
||||
node.addEventListener(event, e => {
|
||||
for (const selector in selectors) {
|
||||
|
|
|
@ -17,4 +17,19 @@ defmodule Philomena.PollOptions.PollOption do
|
|||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def creation_changeset(poll_option, attrs) do
|
||||
poll_option
|
||||
|> cast(attrs, [:label])
|
||||
|> validate_required([:label])
|
||||
|> validate_length(:label, max: 80, count: :bytes)
|
||||
|> unique_constraint(:label, name: :index_poll_options_on_poll_id_and_label)
|
||||
|> ignore_if_blank()
|
||||
end
|
||||
|
||||
defp ignore_if_blank(%{valid?: false, changes: changes} = changeset) when changes == %{},
|
||||
do: %{changeset | action: :ignore}
|
||||
defp ignore_if_blank(changeset),
|
||||
do: changeset
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Philomena.Polls.Poll do
|
|||
field :total_votes, :integer, default: 0
|
||||
field :hidden_from_users, :boolean, default: false
|
||||
field :deletion_reason, :string, default: ""
|
||||
field :until, :string, virtual: true
|
||||
|
||||
timestamps(inserted_at: :created_at)
|
||||
end
|
||||
|
@ -27,4 +28,37 @@ defmodule Philomena.Polls.Poll do
|
|||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def creation_changeset(poll, attrs) do
|
||||
poll
|
||||
|> cast(attrs, [:title, :until, :vote_method])
|
||||
|> put_active_until()
|
||||
|> validate_required([:title, :active_until, :vote_method])
|
||||
|> validate_length(:title, max: 140, count: :bytes)
|
||||
|> validate_inclusion(:vote_method, ["single", "multiple"])
|
||||
|> cast_assoc(:options, with: &PollOption.creation_changeset/2)
|
||||
|> validate_length(:options, min: 2, max: 20)
|
||||
|> ignore_if_blank()
|
||||
end
|
||||
|
||||
defp ignore_if_blank(%{valid?: false, changes: changes} = changeset) when changes == %{},
|
||||
do: %{changeset | action: :ignore}
|
||||
defp ignore_if_blank(changeset),
|
||||
do: changeset
|
||||
|
||||
defp put_active_until(changeset) do
|
||||
changeset
|
||||
|> get_field(:until)
|
||||
|> RelativeDate.Parser.parse()
|
||||
|> case do
|
||||
{:ok, until} ->
|
||||
changeset
|
||||
|> change(active_until: until)
|
||||
|
||||
_error ->
|
||||
changeset
|
||||
|> add_error(:active_until, "invalid date format")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,6 +49,18 @@ defmodule Philomena.Posts.Post do
|
|||
|> put_name_at_post_time()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def topic_creation_changeset(post, attrs, attribution, anonymous?) do
|
||||
post
|
||||
|> change(anonymous: anonymous?)
|
||||
|> cast(attrs, [:body])
|
||||
|> validate_required([:body])
|
||||
|> validate_length(:body, min: 1, max: 300_000, count: :bytes)
|
||||
|> change(attribution)
|
||||
|> change(topic_position: 0)
|
||||
|> put_name_at_post_time()
|
||||
end
|
||||
|
||||
defp put_name_at_post_time(%{changes: %{user: %{data: %{name: name}}}} = changeset),
|
||||
do: change(changeset, name_at_post_time: name)
|
||||
defp put_name_at_post_time(changeset),
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule Philomena.Repo do
|
||||
alias Ecto.Multi
|
||||
|
||||
use Ecto.Repo,
|
||||
otp_app: :philomena,
|
||||
adapter: Ecto.Adapters.Postgres
|
||||
|
@ -11,17 +13,14 @@ defmodule Philomena.Repo do
|
|||
serializable: "SERIALIZABLE"
|
||||
}
|
||||
|
||||
def isolated_transaction(f, level) do
|
||||
Philomena.Repo.transaction(fn ->
|
||||
Philomena.Repo.query!("SET TRANSACTION ISOLATION LEVEL #{@levels[level]}")
|
||||
Philomena.Repo.transaction(f)
|
||||
end)
|
||||
|> case do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
def isolated_transaction(%Multi{} = multi, level) do
|
||||
Multi.append(
|
||||
Multi.new |> Multi.run(:isolate, fn repo, _chg ->
|
||||
repo.query!("SET TRANSACTION ISOLATION LEVEL #{@levels[level]}")
|
||||
{:ok, nil}
|
||||
end),
|
||||
multi
|
||||
)
|
||||
|> Philomena.Repo.transaction()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,39 @@
|
|||
defmodule Philomena.Slug do
|
||||
# Generates a URL-safe slug from a string by removing nonessential
|
||||
# information from it.
|
||||
#
|
||||
# The process for this is as follows:
|
||||
#
|
||||
# 1. Remove non-ASCII or non-printable characters.
|
||||
#
|
||||
# 2. Replace any runs of non-alphanumeric characters that were allowed
|
||||
# through previously with hyphens.
|
||||
#
|
||||
# 3. Remove any starting or ending hyphens.
|
||||
#
|
||||
# 4. Convert all characters to their lowercase equivalents.
|
||||
#
|
||||
# This method makes no guarantee of creating unique slugs for unique inputs.
|
||||
# In addition, for certain inputs, it will return empty strings.
|
||||
#
|
||||
# Example
|
||||
#
|
||||
# destructive_slug("Time-Wasting Thread 3.0 (SFW - No Explicit/Grimdark)")
|
||||
# #=> "time-wasting-thread-3-0-sfw-no-explicit-grimdark"
|
||||
#
|
||||
# destructive_slug("~`!@#$%^&*()-_=+[]{};:'\" <>,./?")
|
||||
# #=> ""
|
||||
#
|
||||
@spec destructive_slug(String.t()) :: String.t()
|
||||
def destructive_slug(input) when is_binary(input) do
|
||||
input
|
||||
|> String.replace(~r/[^ -~]/, "") # 1
|
||||
|> String.replace(~r/[^a-zA-Z0-9]+/, "-") # 2
|
||||
|> String.replace(~r/\A-|-\z/, "") # 3
|
||||
|> String.downcase() # 4
|
||||
end
|
||||
def destructive_slug(_input), do: ""
|
||||
|
||||
def slug(string) when is_binary(string) do
|
||||
string
|
||||
|> String.replace("-", "-dash-")
|
||||
|
|
|
@ -4,22 +4,12 @@ defmodule Philomena.Topics do
|
|||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Ecto.Multi
|
||||
alias Philomena.Repo
|
||||
|
||||
alias Philomena.Topics.Topic
|
||||
|
||||
@doc """
|
||||
Returns the list of topics.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_topics()
|
||||
[%Topic{}, ...]
|
||||
|
||||
"""
|
||||
def list_topics do
|
||||
Repo.all(Topic)
|
||||
end
|
||||
alias Philomena.Forums.Forum
|
||||
alias Philomena.Notifications
|
||||
|
||||
@doc """
|
||||
Gets a single topic.
|
||||
|
@ -49,10 +39,58 @@ defmodule Philomena.Topics do
|
|||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_topic(attrs \\ %{}) do
|
||||
def create_topic(forum, attribution, attrs \\ %{}) do
|
||||
topic =
|
||||
%Topic{}
|
||||
|> Topic.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|> Topic.creation_changeset(attrs, forum, attribution)
|
||||
|
||||
Multi.new
|
||||
|> Multi.insert(:topic, topic)
|
||||
|> Multi.run(:update_topic, fn repo, %{topic: topic} ->
|
||||
{count, nil} =
|
||||
Topic
|
||||
|> where(id: ^topic.id)
|
||||
|> repo.update_all(set: [last_post_id: hd(topic.posts).id])
|
||||
|
||||
{:ok, count}
|
||||
end)
|
||||
|> Multi.run(:update_forum, fn repo, %{topic: topic} ->
|
||||
{count, nil} =
|
||||
Forum
|
||||
|> where(id: ^topic.forum_id)
|
||||
|> repo.update_all(inc: [post_count: 1], set: [last_post_id: hd(topic.posts).id])
|
||||
|
||||
{:ok, count}
|
||||
end)
|
||||
|> Repo.isolated_transaction(:serializable)
|
||||
end
|
||||
|
||||
def notify_topic(topic) do
|
||||
spawn fn ->
|
||||
forum =
|
||||
topic
|
||||
|> Repo.preload(:forum)
|
||||
|> Map.fetch!(:forum)
|
||||
|
||||
subscriptions =
|
||||
forum
|
||||
|> Repo.preload(:subscriptions)
|
||||
|> Map.fetch!(:subscriptions)
|
||||
|
||||
Notifications.notify(
|
||||
topic,
|
||||
subscriptions,
|
||||
%{
|
||||
actor_id: forum.id,
|
||||
actor_type: "Forum",
|
||||
actor_child_id: topic.id,
|
||||
actor_child_type: "Topic",
|
||||
action: "posted a new topic in"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
topic
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Philomena.Topics.Topic do
|
|||
alias Philomena.Polls.Poll
|
||||
alias Philomena.Posts.Post
|
||||
alias Philomena.Topics.Subscription
|
||||
alias Philomena.Slug
|
||||
|
||||
@derive {Phoenix.Param, key: :slug}
|
||||
schema "topics" do
|
||||
|
@ -20,7 +21,7 @@ defmodule Philomena.Topics.Topic do
|
|||
has_many :subscriptions, Subscription
|
||||
|
||||
field :title, :string
|
||||
field :post_count, :integer, default: 0
|
||||
field :post_count, :integer, default: 1
|
||||
field :view_count, :integer, default: 0
|
||||
field :sticky, :boolean, default: false
|
||||
field :last_replied_to_at, :naive_datetime
|
||||
|
@ -40,4 +41,37 @@ defmodule Philomena.Topics.Topic do
|
|||
|> cast(attrs, [])
|
||||
|> validate_required([])
|
||||
end
|
||||
|
||||
@doc false
|
||||
def creation_changeset(topic, attrs, forum, attribution) do
|
||||
changes =
|
||||
topic
|
||||
|> cast(attrs, [:title, :anonymous])
|
||||
|> validate_required([:title, :anonymous])
|
||||
|
||||
anonymous? =
|
||||
changes
|
||||
|> get_field(:anonymous)
|
||||
|
||||
changes
|
||||
|> validate_length(:title, min: 4, max: 96, count: :bytes)
|
||||
|> put_slug()
|
||||
|> change(forum: forum, user: attribution[:user])
|
||||
|> validate_required(:forum)
|
||||
|> cast_assoc(:poll, with: &Poll.creation_changeset/2)
|
||||
|> cast_assoc(:posts, with: {Post, :topic_creation_changeset, [attribution, anonymous?]})
|
||||
|> validate_length(:posts, is: 1)
|
||||
|> unique_constraint(:slug, name: :index_topics_on_forum_id_and_slug)
|
||||
end
|
||||
|
||||
def put_slug(changeset) do
|
||||
slug =
|
||||
changeset
|
||||
|> get_field(:title)
|
||||
|> Slug.destructive_slug()
|
||||
|
||||
changeset
|
||||
|> put_change(:slug, slug)
|
||||
|> validate_required(:slug, message: "must be printable")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
defmodule PhilomenaWeb.TopicController do
|
||||
use PhilomenaWeb, :controller
|
||||
|
||||
alias Philomena.{Forums.Forum, Topics, Topics.Topic, Posts, Posts.Post, Textile.Renderer}
|
||||
alias Philomena.{Forums.Forum, Topics.Topic, Posts.Post, Polls.Poll, PollOptions.PollOption}
|
||||
alias Philomena.{Topics, Posts}
|
||||
alias Philomena.Textile.Renderer
|
||||
alias Philomena.Repo
|
||||
import Ecto.Query
|
||||
|
||||
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create]
|
||||
plug PhilomenaWeb.UserAttributionPlug when action in [:new, :create]
|
||||
plug :load_and_authorize_resource, model: Forum, id_name: "forum_id", id_field: "short_name", persisted: true
|
||||
|
||||
def show(conn, %{"id" => slug} = params) do
|
||||
|
@ -60,4 +64,37 @@ defmodule PhilomenaWeb.TopicController do
|
|||
|
||||
render(conn, "show.html", posts: posts, changeset: changeset, watching: watching)
|
||||
end
|
||||
|
||||
def new(conn, _params) do
|
||||
changeset =
|
||||
%Topic{poll: %Poll{options: [%PollOption{}, %PollOption{}]}, posts: [%Post{}]}
|
||||
|> Topics.change_topic()
|
||||
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
|
||||
def create(conn, %{"topic" => topic_params}) do
|
||||
attributes = conn.assigns.attributes
|
||||
forum = conn.assigns.forum
|
||||
|
||||
case Topics.create_topic(forum, attributes, topic_params) do
|
||||
{:ok, %{topic: topic}} ->
|
||||
post = hd(topic.posts)
|
||||
Posts.reindex_post(post)
|
||||
Topics.notify_topic(topic)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Successfully posted topic.")
|
||||
|> redirect(to: Routes.forum_topic_path(conn, :show, forum, topic))
|
||||
|
||||
{:error, :topic, changeset, _} ->
|
||||
conn
|
||||
|> render("new.html", changeset: changeset)
|
||||
|
||||
_error ->
|
||||
conn
|
||||
|> put_flash(:error, "There was an error with your submission. Please try again.")
|
||||
|> redirect(to: Routes.forum_topic_path(conn, :new, forum))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -67,7 +67,7 @@ defmodule PhilomenaWeb.Router do
|
|||
end
|
||||
|
||||
resources "/forums", ForumController, only: [] do
|
||||
resources "/topics", TopicController, only: [] do
|
||||
resources "/topics", TopicController, only: [:new, :create] do
|
||||
resources "/subscription", Topic.SubscriptionController, only: [:create, :delete], singleton: true
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,10 @@ h1 = @forum.name
|
|||
=> link("Forums", to: Routes.forum_path(@conn, :index))
|
||||
' »
|
||||
=> link(@forum.name, to: Routes.forum_path(@conn, :show, @forum))
|
||||
/= icon_link 'New Topic', 'fas fa-fw fa-pen-square', new_forum_topic_path(@forum)
|
||||
a href=Routes.forum_topic_path(@conn, :new, @forum)
|
||||
i.fa.fa-fw.fa-pencil>
|
||||
' New Topic
|
||||
|
||||
/= icon_link 'Search Posts', 'fa fa-fw fa-search', posts_path(forum_id: @forum.id)
|
||||
span.spacing-left
|
||||
=> @forum.topic_count
|
||||
|
|
85
lib/philomena_web/templates/topic/new.html.slime
Normal file
85
lib/philomena_web/templates/topic/new.html.slime
Normal file
|
@ -0,0 +1,85 @@
|
|||
= form_for @changeset, Routes.forum_topic_path(@conn, :create, @forum), fn f ->
|
||||
= if @changeset.action do
|
||||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
i.fa.fa-pencil>
|
||||
' Create a Topic
|
||||
|
||||
a href="#" data-click-tab="preview"
|
||||
i.fa.fa-eye>
|
||||
' Preview
|
||||
|
||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
||||
.field
|
||||
= text_input f, :title, class: "input input--wide", placeholder: "Title"
|
||||
= error_tag f, :title
|
||||
= error_tag f, :slug
|
||||
|
||||
= inputs_for f, :posts, fn fp ->
|
||||
.field
|
||||
= textarea fp, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for NSFW stuff in SFW forums.", required: true
|
||||
= error_tag fp, :body
|
||||
|
||||
.field
|
||||
=> checkbox f, :anonymous
|
||||
= label f, :anonymous, "Post anonymously"
|
||||
|
||||
= inputs_for f, :poll, fn fp ->
|
||||
#add-poll
|
||||
input.toggle-box id="add_poll" name="add_poll" type="checkbox"
|
||||
label for="add_poll" Add a poll
|
||||
.toggle-box-container
|
||||
p
|
||||
' Polls may have a maximum of
|
||||
span.js-max-option-count> 20
|
||||
' options. Leave any options you don't want to use blank.
|
||||
' Only registered users will be able to vote.
|
||||
|
||||
.field.field--block
|
||||
= text_input fp, :title, class: "input input--wide", placeholder: "Poll title", maxlength: 140
|
||||
= error_tag fp, :title
|
||||
|
||||
p.fieldlabel
|
||||
' End date
|
||||
|
||||
.field.field--block
|
||||
= text_input fp, :until, class: "input input--wide", placeholder: "2 weeks from now", maxlength: 255
|
||||
= error_tag fp, :until
|
||||
= error_tag fp, :active_until
|
||||
|
||||
p.fieldlabel
|
||||
' Specify when the poll should end. Once the poll ends, no more
|
||||
' votes can be cast and the final results will be displayed. Good
|
||||
' values to try are "1 week from now" and "24 hours from now". Polls
|
||||
' must last for at least 24 hours.
|
||||
|
||||
p.fieldlabel
|
||||
' Voting method:
|
||||
|
||||
.field.field--block
|
||||
= select fp, :vote_method, ["-": "", "Single option": :single, "Multiple options": :multiple], class: "input"
|
||||
= error_tag fp, :vote_method
|
||||
|
||||
= inputs_for fp, :options, fn fo ->
|
||||
.field.js-poll-option.field--inline.flex--no-wrap.flex--centered
|
||||
= text_input fo, :label, class: "input flex__grow js-option-label", placeholder: "Option"
|
||||
= error_tag fo, :label
|
||||
|
||||
label.input--separate-left.flex__fixed.flex--centered
|
||||
a.js-option-remove href="#"
|
||||
i.fa.fa-trash>
|
||||
' Delete
|
||||
|
||||
button.button.js-poll-add-option type="button"
|
||||
i.fa.fa-plus>
|
||||
' Add option
|
||||
|
||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
||||
' [Loading preview...]
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
= submit "Post", class: "button"
|
|
@ -23,6 +23,7 @@
|
|||
svg.poll-bar__image width=percent_of_total(option, @poll) height="100%" viewBox="0 0 1 1" preserveAspectRatio="none"
|
||||
rect class=poll_bar_class(option, winning, winners?) width="1" height="1"
|
||||
|
||||
p
|
||||
= if active?(@poll) do
|
||||
' Poll ends
|
||||
= pretty_time(@poll.active_until)
|
||||
|
|
|
@ -13,10 +13,10 @@ defmodule PhilomenaWeb.Topic.PollView do
|
|||
end
|
||||
|
||||
def active?(poll) do
|
||||
not poll.hidden_from_users and poll.active_until > DateTime.utc_now()
|
||||
not poll.hidden_from_users and DateTime.diff(poll.active_until, DateTime.utc_now()) > 0
|
||||
end
|
||||
|
||||
def percent_of_total(_option, %{total_votes: 0}), do: 0
|
||||
def percent_of_total(_option, %{total_votes: 0}), do: "0%"
|
||||
def percent_of_total(%{vote_count: vote_count}, %{total_votes: total_votes}) do
|
||||
:io_lib.format("~.2f%", [(vote_count / total_votes * 100)])
|
||||
end
|
||||
|
|
87
lib/relative_date/parser.ex
Normal file
87
lib/relative_date/parser.ex
Normal file
|
@ -0,0 +1,87 @@
|
|||
defmodule RelativeDate.Parser do
|
||||
import NimbleParsec
|
||||
|
||||
time_specifier =
|
||||
choice([
|
||||
string("second") |> replace(1),
|
||||
string("minute") |> replace(60),
|
||||
string("hour") |> replace(3_600),
|
||||
string("day") |> replace(86_400),
|
||||
string("week") |> replace(604_800),
|
||||
string("month") |> replace(2_592_000),
|
||||
string("year") |> replace(31_536_000)
|
||||
])
|
||||
|> ignore(optional(string("s")))
|
||||
|
||||
direction_specifier =
|
||||
choice([
|
||||
string("ago") |> replace(-1),
|
||||
string("from now") |> replace(1)
|
||||
])
|
||||
|
||||
space = ignore(repeat(string(" ")))
|
||||
|
||||
moon =
|
||||
space
|
||||
|> string("moon")
|
||||
|> concat(space)
|
||||
|> eos()
|
||||
|> unwrap_and_tag(:moon)
|
||||
|
||||
date =
|
||||
space
|
||||
|> integer(min: 1)
|
||||
|> concat(space)
|
||||
|> concat(time_specifier)
|
||||
|> concat(space)
|
||||
|> concat(direction_specifier)
|
||||
|> concat(space)
|
||||
|> eos()
|
||||
|> tag(:relative_date)
|
||||
|
||||
relative_date =
|
||||
choice([
|
||||
moon,
|
||||
date
|
||||
])
|
||||
|
||||
defparsecp :relative_date, relative_date
|
||||
|
||||
def parse(input) do
|
||||
input =
|
||||
input
|
||||
|> to_string()
|
||||
|> String.trim()
|
||||
|
||||
case parse_absolute(input) do
|
||||
{:ok, datetime} ->
|
||||
{:ok, datetime}
|
||||
|
||||
_error ->
|
||||
parse_relative(input)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_absolute(input) do
|
||||
case DateTime.from_iso8601(input) do
|
||||
{:ok, datetime, _offset} ->
|
||||
{:ok, datetime |> DateTime.truncate(:second)}
|
||||
|
||||
_error ->
|
||||
{:error, "Parse error"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_relative(input) do
|
||||
case relative_date(input) do
|
||||
{:ok, [moon: _moon], _1, _2, _3, _4} ->
|
||||
{:ok, DateTime.utc_now() |> DateTime.add(31_536_000_000, :second) |> DateTime.truncate(:second)}
|
||||
|
||||
{:ok, [relative_date: [amount, scale, direction]], _1, _2, _3, _4} ->
|
||||
{:ok, DateTime.utc_now() |> DateTime.add(amount * scale * direction, :second) |> DateTime.truncate(:second)}
|
||||
|
||||
_error ->
|
||||
{:error, "Parse error"}
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue