shiplog 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +62 -4
- data/README.md +178 -7
- data/app/controllers/shiplog/admin/announcements_controller.rb +19 -0
- data/app/controllers/shiplog/admin/entries/announcement_notifications_controller.rb +18 -0
- data/app/controllers/shiplog/admin/entries/exports_controller.rb +45 -0
- data/app/controllers/shiplog/admin/entries/newsletter_retries_controller.rb +17 -0
- data/app/controllers/shiplog/admin/entries/newsletter_tests_controller.rb +39 -0
- data/app/controllers/shiplog/admin/entries/newsletters_controller.rb +40 -0
- data/app/controllers/shiplog/admin/entries/translations_controller.rb +1 -0
- data/app/controllers/shiplog/admin/entries_controller.rb +7 -3
- data/app/controllers/shiplog/admin/newsletter_subscribers_controller.rb +47 -0
- data/app/controllers/shiplog/changelog_entries_controller.rb +14 -2
- data/app/controllers/shiplog/changelog_feeds_controller.rb +6 -2
- data/app/controllers/shiplog/changelogs_controller.rb +11 -1
- data/app/controllers/shiplog/frontends_controller.rb +2 -1
- data/app/controllers/shiplog/newsletter/confirmations_controller.rb +27 -0
- data/app/controllers/shiplog/newsletter/preferences_controller.rb +50 -0
- data/app/controllers/shiplog/newsletter/subscriptions_controller.rb +70 -0
- data/app/controllers/shiplog/newsletter/unsubscriptions_controller.rb +36 -0
- data/app/controllers/shiplog/newsletter_controller.rb +34 -0
- data/app/controllers/shiplog/public_controller.rb +10 -0
- data/app/frontend/shiplog/admin.css +5 -0
- data/app/frontend/shiplog/once.js +31 -0
- data/app/frontend/shiplog/shiplog.css +72 -0
- data/app/helpers/shiplog/admin/entries_helper.rb +2 -0
- data/app/helpers/shiplog/head_helper.rb +1 -1
- data/app/helpers/shiplog/newsletter_helper.rb +28 -0
- data/app/helpers/shiplog/newsletter_mailer_helper.rb +37 -0
- data/app/helpers/shiplog/permalink_helper.rb +24 -5
- data/app/helpers/shiplog/unseen_helper.rb +4 -0
- data/app/jobs/shiplog/announce_job.rb +30 -0
- data/app/jobs/shiplog/draft_announcement_job.rb +9 -0
- data/app/jobs/shiplog/purge_newsletter_job.rb +15 -0
- data/app/jobs/shiplog/send_newsletter_job.rb +64 -0
- data/app/jobs/shiplog/translate_entry_job.rb +2 -0
- data/app/mailers/shiplog/newsletter_mailer.rb +51 -0
- data/app/models/shiplog/announcements.rb +34 -0
- data/app/models/shiplog/area_detector.rb +2 -1
- data/app/models/shiplog/change_candidates.rb +4 -0
- data/app/models/shiplog/drafter.rb +4 -2
- data/app/models/shiplog/entry.rb +76 -6
- data/app/models/shiplog/markdown.rb +233 -0
- data/app/models/shiplog/newsletter/content.rb +172 -0
- data/app/models/shiplog/newsletter/csv.rb +32 -0
- data/app/models/shiplog/newsletter/recipients.rb +92 -0
- data/app/models/shiplog/newsletter/tokens.rb +77 -0
- data/app/models/shiplog/newsletter.rb +180 -0
- data/app/models/shiplog/newsletter_delivery.rb +83 -0
- data/app/models/shiplog/newsletter_subscriber.rb +158 -0
- data/app/models/shiplog/period.rb +129 -0
- data/app/models/shiplog/public_links.rb +47 -0
- data/app/models/shiplog/rewriter/announcement_prompt.rb +70 -0
- data/app/models/shiplog/rewriter.rb +23 -3
- data/app/models/shiplog/translator/prompt.rb +2 -2
- data/app/models/shiplog/translator.rb +1 -1
- data/app/views/shiplog/admin/_nav.html.erb +3 -0
- data/app/views/shiplog/admin/entries/_announcement.html.erb +52 -0
- data/app/views/shiplog/admin/entries/_badges.html.erb +1 -1
- data/app/views/shiplog/admin/entries/_newsletter.html.erb +42 -0
- data/app/views/shiplog/admin/entries/edit.html.erb +8 -2
- data/app/views/shiplog/admin/entries/index.html.erb +9 -0
- data/app/views/shiplog/admin/entries/newsletters/show.html.erb +46 -0
- data/app/views/shiplog/admin/newsletter_subscribers/index.html.erb +83 -0
- data/app/views/shiplog/changelog_entries/announcement.html.erb +16 -0
- data/app/views/shiplog/changelogs/_month.html.erb +10 -3
- data/app/views/shiplog/changelogs/show.html.erb +4 -1
- data/app/views/shiplog/newsletter/_form.html.erb +25 -0
- data/app/views/shiplog/newsletter/_page.html.erb +14 -0
- data/app/views/shiplog/newsletter/confirmations/create.html.erb +5 -0
- data/app/views/shiplog/newsletter/confirmations/show.html.erb +5 -0
- data/app/views/shiplog/newsletter/invalid.html.erb +4 -0
- data/app/views/shiplog/newsletter/preferences/destroy.html.erb +4 -0
- data/app/views/shiplog/newsletter/preferences/show.html.erb +23 -0
- data/app/views/shiplog/newsletter/rate_limited.html.erb +4 -0
- data/app/views/shiplog/newsletter/subscriptions/show.html.erb +16 -0
- data/app/views/shiplog/newsletter/unsubscriptions/create.html.erb +4 -0
- data/app/views/shiplog/newsletter/unsubscriptions/show.html.erb +5 -0
- data/app/views/shiplog/newsletter_mailer/announcement.html.erb +113 -0
- data/app/views/shiplog/newsletter_mailer/announcement.text.erb +38 -0
- data/app/views/shiplog/newsletter_mailer/confirmation.html.erb +43 -0
- data/app/views/shiplog/newsletter_mailer/confirmation.text.erb +12 -0
- data/config/admin_routes.rb +12 -0
- data/config/locales/en.yml +147 -0
- data/config/locales/es.yml +147 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20261028000001_shiplog_0_4_0.rb +19 -0
- data/db/migrate/20261028000002_shiplog_0_4_0_newsletter.rb +44 -0
- data/lib/generators/shiplog/install/install_generator.rb +14 -1
- data/lib/generators/shiplog/install/templates/initializer.rb.tt +50 -0
- data/lib/shiplog/classes.rb +14 -1
- data/lib/shiplog/configuration.rb +135 -3
- data/lib/shiplog/engine.rb +2 -0
- data/{app/helpers → lib}/shiplog/head_tags_helper.rb +15 -5
- data/lib/shiplog/previews/shiplog/newsletter_mailer_preview.rb +77 -0
- data/lib/shiplog/version.rb +1 -1
- data/lib/shiplog.rb +33 -0
- data/lib/tasks/shiplog.rake +22 -0
- metadata +57 -2
|
@@ -4,6 +4,8 @@ module Shiplog
|
|
|
4
4
|
PARSE_ERROR = "Could not parse rewriter output".freeze
|
|
5
5
|
MAX_TITLE_CHARS = 120
|
|
6
6
|
MAX_BODY_CHARS = 2_000
|
|
7
|
+
# An announcement's Markdown body covers a whole month.
|
|
8
|
+
MAX_ANNOUNCEMENT_BODY_CHARS = 10_000
|
|
7
9
|
MAX_ROADMAP_BODY_CHARS = 220
|
|
8
10
|
# A period after a common abbreviation ("e.g.", "i.e.", "approx.", "etc.") does not end a sentence.
|
|
9
11
|
SENTENCE_END = /(?<!\be\.g)(?<!\bi\.e)(?<!\bapprox)(?<!\betc)(?<!\bvs)(?<!\bcf)[.!?](?=\s|\z)/i
|
|
@@ -38,6 +40,9 @@ module Shiplog
|
|
|
38
40
|
|
|
39
41
|
def self.sanitize(text) = text.tr("\t", " ").gsub(CONTROL_CHARS, "")
|
|
40
42
|
|
|
43
|
+
# The longest body an entry of this kind may have.
|
|
44
|
+
def self.max_body_chars(entry) = entry.announcement? ? MAX_ANNOUNCEMENT_BODY_CHARS : MAX_BODY_CHARS
|
|
45
|
+
|
|
41
46
|
def initialize(entry)
|
|
42
47
|
@entry = entry
|
|
43
48
|
end
|
|
@@ -46,6 +51,7 @@ module Shiplog
|
|
|
46
51
|
# call replaces the text of an approved entry but returns it to draft for re-approval.
|
|
47
52
|
def call(force: false)
|
|
48
53
|
return @entry if @entry.approved? && !force
|
|
54
|
+
return write_skeleton(force) if @entry.announcement? && Shiplog.config.rewriter.nil?
|
|
49
55
|
|
|
50
56
|
result = parse(request)
|
|
51
57
|
locked(force) { result ? apply(result) : fail_with(PARSE_ERROR) }
|
|
@@ -56,6 +62,19 @@ module Shiplog
|
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
private
|
|
65
|
+
# Without a rewriter, an announcement's draft is its month's skeleton (Period#title and #body), written fresh
|
|
66
|
+
# so that regenerating it picks up the month's current entries. Not an error: it is the documented draft.
|
|
67
|
+
def write_skeleton(force)
|
|
68
|
+
locked(force) do
|
|
69
|
+
period = @entry.subject
|
|
70
|
+
@entry.assign_attributes(title: period.title, body: period.body, rewritten_at: Time.current, rewrite_error: nil,
|
|
71
|
+
source_digest: Entry.digest_for(period))
|
|
72
|
+
@entry.assign_attributes(status: "draft", approved_at: nil, approved_by: nil) if @entry.approved?
|
|
73
|
+
@entry.save!
|
|
74
|
+
end
|
|
75
|
+
@entry
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
# Re-checks state under a row lock: the entry may have been approved or edited during the LLM call.
|
|
60
79
|
def locked(force)
|
|
61
80
|
@entry.with_lock do
|
|
@@ -68,7 +87,8 @@ module Shiplog
|
|
|
68
87
|
rewriter = Shiplog.config.rewriter
|
|
69
88
|
raise "No rewriter configured" unless rewriter
|
|
70
89
|
|
|
71
|
-
|
|
90
|
+
prompt = @entry.announcement? ? AnnouncementPrompt.new(@entry) : Prompt.new(@entry)
|
|
91
|
+
rewriter.call(prompt.to_s).to_s
|
|
72
92
|
end
|
|
73
93
|
|
|
74
94
|
def parse(reply) = self.class.first_valid_object(reply) { |data| validate(data) }
|
|
@@ -85,7 +105,7 @@ module Shiplog
|
|
|
85
105
|
end
|
|
86
106
|
|
|
87
107
|
def limit_body(body)
|
|
88
|
-
return body.truncate(
|
|
108
|
+
return body.truncate(self.class.max_body_chars(@entry), separator: " ", omission: "…") unless @entry.roadmap?
|
|
89
109
|
|
|
90
110
|
shorten_roadmap(body)
|
|
91
111
|
end
|
|
@@ -108,7 +128,7 @@ module Shiplog
|
|
|
108
128
|
# nothing (cap reached, invalid) keeps the entry's current area: only an explicit valid name changes it. No
|
|
109
129
|
# area in the reply keeps it too, and one an admin chose is never changed.
|
|
110
130
|
def assign_area(name)
|
|
111
|
-
return if name.nil? || @entry.area_set_by_admin?
|
|
131
|
+
return if name.nil? || @entry.area_set_by_admin? || @entry.announcement?
|
|
112
132
|
|
|
113
133
|
area = Area.resolve_or_create(name)
|
|
114
134
|
@entry.area = area if area
|
|
@@ -8,7 +8,7 @@ module Shiplog
|
|
|
8
8
|
TEMPLATE = <<~PROMPT.freeze
|
|
9
9
|
You translate release notes%<product>s into %<language>s (locale %<locale>s).%<audience>s
|
|
10
10
|
Tone to keep: %<tone>s
|
|
11
|
-
Translate the title and the body inside <source>. Keep the meaning, the tone, product names, numbers, links, line breaks and formatting; do not add, remove or explain anything.
|
|
11
|
+
Translate the title and the body inside <source>. Keep the meaning, the tone, product names, numbers, links, line breaks and formatting (Markdown stays Markdown, link URLs unchanged); do not add, remove or explain anything.
|
|
12
12
|
Leave product names, code and anything in backticks untranslated. Write the way a native speaker would, with no word-for-word calques.
|
|
13
13
|
%<length_rule>s
|
|
14
14
|
Reply with JSON only: {"title": "...", "body": "..."}
|
|
@@ -33,7 +33,7 @@ module Shiplog
|
|
|
33
33
|
format(TEMPLATE, product: config.product_name.present? ? " for #{config.product_name}" : "", language: Translator.language_name(@locale),
|
|
34
34
|
locale: @locale, audience: config.audience.present? ? " The readers are #{config.audience}." : "", tone: config.tone,
|
|
35
35
|
length_rule: @entry.roadmap? ? ROADMAP_LENGTH_RULE : LENGTH_RULE,
|
|
36
|
-
title: Rewriter::Prompt.clean(@entry.title, MAX_TITLE_CHARS), body: Rewriter::Prompt.clean(@entry.body, Rewriter
|
|
36
|
+
title: Rewriter::Prompt.clean(@entry.title, MAX_TITLE_CHARS), body: Rewriter::Prompt.clean(@entry.body, Rewriter.max_body_chars(@entry)))
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -71,7 +71,7 @@ module Shiplog
|
|
|
71
71
|
def limit_body(body)
|
|
72
72
|
return Rewriter.shorten_roadmap(body) if @entry.roadmap?
|
|
73
73
|
|
|
74
|
-
body.truncate(Rewriter
|
|
74
|
+
body.truncate(Rewriter.max_body_chars(@entry), separator: " ", omission: "…")
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Writes under the entry's row lock, after checking again: the entry may have been unpublished, edited or
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
<li><%= link_to t("shiplog.admin.nav.drafts"), root_path, class: "shiplog-admin-nav__link" %></li>
|
|
6
6
|
<li><%= link_to t("shiplog.admin.nav.board"), board_path, class: "shiplog-admin-nav__link" %></li>
|
|
7
7
|
<li><%= link_to t("shiplog.admin.nav.areas", default: "Areas"), areas_path, class: "shiplog-admin-nav__link" %></li>
|
|
8
|
+
<% if Shiplog.newsletter? %>
|
|
9
|
+
<li><%= link_to t("shiplog.admin.nav.newsletter", default: "Newsletter"), newsletter_subscribers_path, class: "shiplog-admin-nav__link" %></li>
|
|
10
|
+
<% end %>
|
|
8
11
|
<li><%= link_to t("shiplog.admin.nav.sync"), sync_path, class: "shiplog-admin-nav__link" %></li>
|
|
9
12
|
</ul>
|
|
10
13
|
</nav>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<%# locals: (entry:) %>
|
|
2
|
+
<%# An announcement's preview (its Markdown rendered as on the public page), where it is published, whether the
|
|
3
|
+
host was told (config.on_announcement_approved), the export links and, with config.newsletter, the newsletter. %>
|
|
4
|
+
<section class="shiplog-admin-panel">
|
|
5
|
+
<h2><%= t("shiplog.admin.announcements.publishing") %></h2>
|
|
6
|
+
<% if entry.approved? %>
|
|
7
|
+
<p>
|
|
8
|
+
<% if Shiplog.announcement_pages? && entry.public_url %>
|
|
9
|
+
<%= link_to t("shiplog.admin.announcements.view_page"), entry.public_url, target: "_blank", rel: "noopener" %>
|
|
10
|
+
<% elsif Shiplog.config.announcements == :external %>
|
|
11
|
+
<%= t("shiplog.admin.announcements.external") %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</p>
|
|
14
|
+
<p class="shiplog-admin__muted">
|
|
15
|
+
<% if entry.announced_at %>
|
|
16
|
+
<%= t("shiplog.admin.announcements.announced", time: l(entry.announced_at, format: :short)) %>
|
|
17
|
+
<% elsif !entry.translations_ready? %>
|
|
18
|
+
<%= t("shiplog.admin.announcements.waiting_for_translations") %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= t("shiplog.admin.announcements.announcing") %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</p>
|
|
23
|
+
<% if entry.announced_at.nil? && Shiplog.announcements? %>
|
|
24
|
+
<%= button_to t("shiplog.admin.announcements.notify_now"), entry_announcement_notification_path(entry, inbox_filter_params), class: "shiplog-button" %>
|
|
25
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.announcements.notify_now_hint") %></p>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% else %>
|
|
28
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.announcements.not_approved") %></p>
|
|
29
|
+
<% end %>
|
|
30
|
+
|
|
31
|
+
<p>
|
|
32
|
+
<%= t("shiplog.admin.announcements.export") %>
|
|
33
|
+
<% [ I18n.default_locale.to_s, *Shiplog.config.locales.map(&:to_s) ].each do |locale| %>
|
|
34
|
+
<span class="shiplog-admin-export">
|
|
35
|
+
<% if Shiplog.config.locales.any? %><strong><%= locale %></strong><% end %>
|
|
36
|
+
<% { "markdown" => "Markdown", "html" => "HTML", "text" => t("shiplog.admin.announcements.plain_text") }.each do |format, label| %>
|
|
37
|
+
<%= link_to label, entry_export_path(entry, as: format, export_locale: (locale unless locale == I18n.default_locale.to_s)) %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</span>
|
|
40
|
+
<% end %>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<% if Shiplog.newsletter? %>
|
|
44
|
+
<%= render "shiplog/admin/entries/newsletter", entry: %>
|
|
45
|
+
<% end %>
|
|
46
|
+
|
|
47
|
+
<h3><%= t("shiplog.admin.announcements.preview") %></h3>
|
|
48
|
+
<div class="shiplog-admin-preview">
|
|
49
|
+
<h2><%= entry.title %></h2>
|
|
50
|
+
<%= Shiplog::Markdown.to_html(entry.body, heading_offset: 1) %>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<% if entry.source_changed? %>
|
|
2
|
-
<span class="shiplog-badge shiplog-badge--warning"><%= t("shiplog.admin.entries.source_changed") %></span>
|
|
2
|
+
<span class="shiplog-badge shiplog-badge--warning"><%= t(entry.announcement? ? "shiplog.admin.entries.entries_changed" : "shiplog.admin.entries.source_changed") %></span>
|
|
3
3
|
<% end %>
|
|
4
4
|
<% if entry.draft? && entry.rewritten_at.nil? && entry.rewrite_error.blank? %>
|
|
5
5
|
<span class="shiplog-badge"><%= t("shiplog.admin.entries.not_rewritten") %></span>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%# locals: (entry:) %>
|
|
2
|
+
<%# The newsletter section of an announcement's panel: how the send is going, "Send test" to the admin's address
|
|
3
|
+
in a chosen locale, and "Send" (the confirmation step) when nothing blocks it, else why it is disabled. %>
|
|
4
|
+
<% blockers = Shiplog::Newsletter.blockers(entry) %>
|
|
5
|
+
<% progress = Shiplog::NewsletterDelivery.progress(entry) %>
|
|
6
|
+
<% admin_email = current_admin_name.to_s.strip %>
|
|
7
|
+
<% admin_email = nil unless URI::MailTo::EMAIL_REGEXP.match?(admin_email) %>
|
|
8
|
+
<% locales = Shiplog::NewsletterSubscriber.allowed_locales %>
|
|
9
|
+
<h3><%= t("shiplog.admin.newsletter.heading") %></h3>
|
|
10
|
+
<p class="shiplog-admin__muted shiplog-admin-newsletter__progress">
|
|
11
|
+
<% if progress.values_at(:sent, :failed, :pending).sum.zero? %>
|
|
12
|
+
<%= t("shiplog.admin.newsletter.not_sent") %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<%= t("shiplog.admin.newsletter.progress", **progress.slice(:sent, :failed, :pending)) %>
|
|
15
|
+
<% if progress[:stalled].positive? %>
|
|
16
|
+
<%= t("shiplog.admin.newsletter.stalled", count: progress[:stalled]) %>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</p>
|
|
20
|
+
<div class="shiplog-admin-actions">
|
|
21
|
+
<%= shiplog_javascript_tag "once" %>
|
|
22
|
+
<%= form_with url: entry_newsletter_test_path(entry, inbox_filter_params), class: "shiplog-admin-inline-form", data: { shiplog_once: true } do |form| %>
|
|
23
|
+
<%# test_locale, not locale: a host's around_action that switches the locale from params[:locale] must not read it %>
|
|
24
|
+
<%= form.label :test_locale, t("shiplog.admin.newsletter.test_locale") %>
|
|
25
|
+
<%= form.select :test_locale, locales.map { |locale| [ translation_language_label(locale), locale ] } %>
|
|
26
|
+
<%= form.submit t("shiplog.admin.newsletter.test", email: admin_email || "?"), class: "shiplog-button", disabled: admin_email.nil? %>
|
|
27
|
+
<% if admin_email.nil? %>
|
|
28
|
+
<span class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.test_no_email") %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
31
|
+
<div class="shiplog-admin-inline-form">
|
|
32
|
+
<% if blockers.any? %>
|
|
33
|
+
<button type="button" class="shiplog-button shiplog-button--primary" disabled><%= t("shiplog.admin.newsletter.send") %></button>
|
|
34
|
+
<span class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.send_blocked", reasons: blockers.map { |blocker| t("shiplog.admin.newsletter.blockers.#{blocker}", locales: Shiplog::Newsletter.missing_locales(entry).join(", ")) }.join(", ")) %></span>
|
|
35
|
+
<% else %>
|
|
36
|
+
<%= link_to t("shiplog.admin.newsletter.send"), entry_newsletter_path(entry, inbox_filter_params), class: "shiplog-button shiplog-button--primary" %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% if progress[:failed].positive? || progress[:stalled].positive? %>
|
|
39
|
+
<%= button_to t("shiplog.admin.newsletter.retry"), entry_newsletter_retry_path(entry, inbox_filter_params), class: "shiplog-button", form: { data: { shiplog_once: true } } %>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
</p>
|
|
21
21
|
<p>
|
|
22
22
|
<%= form.label :body, t("shiplog.admin.entries.fields.body") %>
|
|
23
|
-
<%= form.text_area :body, rows: 8 %>
|
|
23
|
+
<%= form.text_area :body, rows: (@entry.announcement? ? 24 : 8) %>
|
|
24
24
|
</p>
|
|
25
25
|
<% if Shiplog::Entry::CHANGELOG_KINDS.include?(@entry.kind) %>
|
|
26
26
|
<p>
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
<%= form.select :changelog_kind, Shiplog::Entry::CHANGELOG_KINDS.map { |kind| [ t("shiplog.admin.entries.category_options.#{kind}"), kind ] } %>
|
|
29
29
|
</p>
|
|
30
30
|
<% end %>
|
|
31
|
+
<% unless @entry.announcement? %>
|
|
31
32
|
<p>
|
|
32
33
|
<%= form.label :area_id, t("shiplog.admin.entries.fields.area", default: "Area") %>
|
|
33
34
|
<%= form.collection_select :area_id, Shiplog::Area.ordered, :id, :name, include_blank: t("shiplog.admin.entries.no_area", default: "None") %>
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
<%= form.label :released_on, t("shiplog.admin.entries.fields.released_on") %>
|
|
37
38
|
<%= form.date_field :released_on %>
|
|
38
39
|
</p>
|
|
40
|
+
<% end %>
|
|
39
41
|
<%= form.submit t("shiplog.admin.entries.save"), class: "shiplog-button shiplog-button--primary" %>
|
|
40
42
|
<% end %>
|
|
41
43
|
|
|
@@ -67,11 +69,15 @@
|
|
|
67
69
|
</aside>
|
|
68
70
|
</div>
|
|
69
71
|
|
|
72
|
+
<% if @entry.announcement? %>
|
|
73
|
+
<%= render "shiplog/admin/entries/announcement", entry: @entry %>
|
|
74
|
+
<% end %>
|
|
75
|
+
|
|
70
76
|
<% if Shiplog.config.locales.any? %>
|
|
71
77
|
<%= render "shiplog/admin/entries/translations", entry: @entry %>
|
|
72
78
|
<% end %>
|
|
73
79
|
|
|
74
|
-
<% if Shiplog.images? %>
|
|
80
|
+
<% if Shiplog.images? && !@entry.announcement? %>
|
|
75
81
|
<%= render "shiplog/admin/entries/images", entry: @entry %>
|
|
76
82
|
<% end %>
|
|
77
83
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
<%= render layout: "shiplog/admin/page" do %>
|
|
2
2
|
<h1 class="shiplog-admin__title"><%= t("shiplog.admin.entries.title") %></h1>
|
|
3
3
|
|
|
4
|
+
<% if Shiplog.announcements? %>
|
|
5
|
+
<div class="shiplog-admin-actions">
|
|
6
|
+
<% Shiplog::Announcements.draftable_months.each do |month| %>
|
|
7
|
+
<%= button_to t("shiplog.admin.announcements.draft", month: l(month, format: "%B %Y")), announcements_path(month: month.strftime("%Y-%m"), **inbox_filter_params), class: "shiplog-button" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
4
12
|
<nav class="shiplog-admin-filters" aria-label="<%= t("shiplog.admin.entries.filters") %>">
|
|
5
13
|
<ul class="shiplog-admin-filters__list">
|
|
6
14
|
<% Shiplog::Entry.statuses.each_key do |status| %>
|
|
@@ -12,6 +20,7 @@
|
|
|
12
20
|
<li><%= link_to t("shiplog.admin.entries.all_kinds"), entries_filter_path(status: @status),
|
|
13
21
|
class: "shiplog-admin-filters__link", "aria-current": (@kind.nil? ? "true" : nil) %></li>
|
|
14
22
|
<% Shiplog::Entry.kinds.each_key do |kind| %>
|
|
23
|
+
<% next if kind == "announcement" && !Shiplog.announcements? %>
|
|
15
24
|
<li><%= link_to t("shiplog.admin.entries.kinds.#{kind}"), entries_filter_path(status: @status, kind:),
|
|
16
25
|
class: "shiplog-admin-filters__link", "aria-current": (kind == @kind ? "true" : nil) %></li>
|
|
17
26
|
<% end %>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<%# The confirmation step before sending: the recipients per locale (after deduplication), what was sent already, and the Send button %>
|
|
2
|
+
<%= render layout: "shiplog/admin/page" do %>
|
|
3
|
+
<h1 class="shiplog-admin__title"><%= t("shiplog.admin.newsletter.confirm_title", title: @entry.title) %></h1>
|
|
4
|
+
|
|
5
|
+
<% if @blockers.any? %>
|
|
6
|
+
<div class="shiplog-admin__flash shiplog-admin__flash--alert" role="alert">
|
|
7
|
+
<%= t("shiplog.admin.newsletter.send_blocked", reasons: @blockers.map { |blocker| t("shiplog.admin.newsletter.blockers.#{blocker}", locales: Shiplog::Newsletter.missing_locales(@entry).join(", ")) }.join(", ")) %>
|
|
8
|
+
</div>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<section class="shiplog-admin-panel">
|
|
12
|
+
<h2><%= t("shiplog.admin.newsletter.recipients") %></h2>
|
|
13
|
+
<% if @recipients_error %>
|
|
14
|
+
<p class="shiplog-admin__flash shiplog-admin__flash--alert" role="alert"><%= t("shiplog.admin.newsletter.recipients_error", error: @recipients_error) %></p>
|
|
15
|
+
<% elsif @counts.empty? %>
|
|
16
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.recipients_none") %></p>
|
|
17
|
+
<% else %>
|
|
18
|
+
<table class="shiplog-admin-table shiplog-admin-counts">
|
|
19
|
+
<thead>
|
|
20
|
+
<tr>
|
|
21
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.locale") %></th>
|
|
22
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.recipients") %></th>
|
|
23
|
+
</tr>
|
|
24
|
+
</thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% @counts.each do |locale, count| %>
|
|
27
|
+
<tr><td><%= locale %></td><td class="shiplog-admin-counts__number"><%= count %></td></tr>
|
|
28
|
+
<% end %>
|
|
29
|
+
<tr><th scope="row"><%= t("shiplog.admin.newsletter.total") %></th><td class="shiplog-admin-counts__number"><%= @counts.values.sum %></td></tr>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% total = @counts.values.sum %>
|
|
34
|
+
<% if @progress.values_at(:sent, :failed, :pending).sum.positive? %>
|
|
35
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.progress", **@progress.slice(:sent, :failed, :pending)) %> <%= t("shiplog.admin.newsletter.confirm_resend_help") %></p>
|
|
36
|
+
<% end %>
|
|
37
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.confirm_help") %></p>
|
|
38
|
+
<div class="shiplog-admin-actions">
|
|
39
|
+
<%= shiplog_javascript_tag "once" %>
|
|
40
|
+
<%= button_to t("shiplog.admin.newsletter.confirm_button", count: total), entry_newsletter_path(@entry, inbox_filter_params), method: :post,
|
|
41
|
+
class: "shiplog-button shiplog-button--primary", disabled: @blockers.any? || total.zero?, form: { data: { shiplog_once: true } } %>
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
|
|
45
|
+
<p><%= link_to t("shiplog.admin.newsletter.back"), edit_entry_path(@entry, inbox_filter_params) %></p>
|
|
46
|
+
<% end %>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<%= render layout: "shiplog/admin/page" do %>
|
|
2
|
+
<h1 class="shiplog-admin__title"><%= t("shiplog.admin.newsletter.title") %></h1>
|
|
3
|
+
<p class="shiplog-admin__muted"><%= t("shiplog.admin.newsletter.help") %></p>
|
|
4
|
+
|
|
5
|
+
<table class="shiplog-admin-table shiplog-admin-counts">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.locale") %></th>
|
|
9
|
+
<% Shiplog::NewsletterSubscriber::STATUSES.each do |status| %>
|
|
10
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.statuses.#{status}") %></th>
|
|
11
|
+
<% end %>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<% @counts.sort_by { |locale, _| locale }.each do |locale, counts| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td><%= locale %></td>
|
|
18
|
+
<% Shiplog::NewsletterSubscriber::STATUSES.each do |status| %>
|
|
19
|
+
<td class="shiplog-admin-counts__number"><%= counts[status] %></td>
|
|
20
|
+
<% end %>
|
|
21
|
+
</tr>
|
|
22
|
+
<% end %>
|
|
23
|
+
<tr>
|
|
24
|
+
<th scope="row"><%= t("shiplog.admin.newsletter.total") %></th>
|
|
25
|
+
<% Shiplog::NewsletterSubscriber::STATUSES.each do |status| %>
|
|
26
|
+
<td class="shiplog-admin-counts__number"><%= @counts.values.sum { |counts| counts[status] } %></td>
|
|
27
|
+
<% end %>
|
|
28
|
+
</tr>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<div class="shiplog-admin-actions">
|
|
33
|
+
<%= form_with url: newsletter_subscribers_path, method: :get, class: "shiplog-admin-inline-form" do |form| %>
|
|
34
|
+
<%= form.label :q, t("shiplog.admin.newsletter.search"), class: "shiplog-admin__visually-hidden" %>
|
|
35
|
+
<%= form.search_field :q, value: @query, placeholder: t("shiplog.admin.newsletter.search_placeholder") %>
|
|
36
|
+
<%= form.submit t("shiplog.admin.newsletter.search"), class: "shiplog-button" %>
|
|
37
|
+
<% if @query.present? %>
|
|
38
|
+
<%= link_to t("shiplog.admin.newsletter.clear"), newsletter_subscribers_path, class: "shiplog-button" %>
|
|
39
|
+
<% end %>
|
|
40
|
+
<% end %>
|
|
41
|
+
<p><%= link_to t("shiplog.admin.newsletter.export"), newsletter_subscribers_path(format: :csv, q: @query.presence), class: "shiplog-button" %></p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<% if @subscribers.empty? %>
|
|
45
|
+
<p class="shiplog-admin__empty"><%= @query.present? ? t("shiplog.admin.newsletter.no_match", query: @query) : t("shiplog.admin.newsletter.empty") %></p>
|
|
46
|
+
<% else %>
|
|
47
|
+
<table class="shiplog-admin-table">
|
|
48
|
+
<thead>
|
|
49
|
+
<tr>
|
|
50
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.email") %></th>
|
|
51
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.locale") %></th>
|
|
52
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.status") %></th>
|
|
53
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.source") %></th>
|
|
54
|
+
<th scope="col"><%= t("shiplog.admin.newsletter.columns.subscribed_at") %></th>
|
|
55
|
+
<th scope="col"><%= t("shiplog.admin.entries.columns.actions") %></th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<% @subscribers.each do |subscriber| %>
|
|
60
|
+
<tr id="shiplog-subscriber-<%= subscriber.id %>">
|
|
61
|
+
<td><%= subscriber.email %></td>
|
|
62
|
+
<td><%= subscriber.locale %></td>
|
|
63
|
+
<td><%= t("shiplog.admin.newsletter.statuses.#{subscriber.status}") %></td>
|
|
64
|
+
<td><%= subscriber.source %></td>
|
|
65
|
+
<td><%= l(subscriber.created_at, format: :short) %></td>
|
|
66
|
+
<td class="shiplog-admin-table__actions">
|
|
67
|
+
<%= button_to t("shiplog.admin.newsletter.remove"), newsletter_subscriber_path(subscriber, q: @query.presence), method: :delete, class: "shiplog-button" %>
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<% end %>
|
|
71
|
+
</tbody>
|
|
72
|
+
</table>
|
|
73
|
+
<% end %>
|
|
74
|
+
|
|
75
|
+
<nav class="shiplog-admin-pagination" aria-label="<%= t("shiplog.admin.entries.pagination") %>">
|
|
76
|
+
<% if @page > 1 %>
|
|
77
|
+
<%= link_to t("shiplog.admin.entries.newer"), newsletter_subscribers_path(q: @query.presence, page: (@page - 1 if @page > 2)), class: "shiplog-admin-pagination__prev" %>
|
|
78
|
+
<% end %>
|
|
79
|
+
<% if @more %>
|
|
80
|
+
<%= link_to t("shiplog.admin.entries.older"), newsletter_subscribers_path(q: @query.presence, page: @page + 1), class: "shiplog-admin-pagination__next" %>
|
|
81
|
+
<% end %>
|
|
82
|
+
</nav>
|
|
83
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%# A month's announcement: its title and Markdown body (Shiplog::Markdown, which escapes everything it does not build) %>
|
|
2
|
+
<% content_for :title, t("shiplog.permalink.title", title: @entry.display_title, changelog: t("shiplog.changelog.title", default: "Changelog"), default: "%{title} · %{changelog}") %>
|
|
3
|
+
<% shiplog_head_page :permalink, title: @entry.display_title, entry: @entry, url: shiplog_entry_url(@entry), json_ld: shiplog_article_json_ld(@entry) %>
|
|
4
|
+
<%= render layout: "shiplog/page", locals: { name: "announcement" } do %>
|
|
5
|
+
<%= render "shiplog/page_header", title: t("shiplog.changelog.title", default: "Changelog"), lede: nil %>
|
|
6
|
+
<article class="<%= shiplog_class(:announcement) %>">
|
|
7
|
+
<span class="<%= shiplog_class(:entry_category) %>"><%= t("shiplog.categories.announcement", default: "Monthly update") %></span>
|
|
8
|
+
<time class="<%= shiplog_class(:entry_date) %>" datetime="<%= @entry.released_on.beginning_of_month.strftime("%Y-%m") %>"><%= l(@entry.released_on, format: "%B %Y") %></time>
|
|
9
|
+
<h2 class="<%= shiplog_class(:announcement_title) %>"><%= @entry.display_title %></h2>
|
|
10
|
+
<div class="<%= shiplog_class(:announcement_body) %>"><%= Shiplog::Markdown.to_html(@entry.display_body, heading_offset: 1) %></div>
|
|
11
|
+
</article>
|
|
12
|
+
<% if Shiplog.newsletter? %>
|
|
13
|
+
<%= render "shiplog/newsletter/form", source: request.path %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<%= link_to t("shiplog.announcements.back", default: "← Back to the changelog"), changelog_path(anchor: shiplog_month_anchor(@entry.released_on)), class: shiplog_class(:permalink_back) %>
|
|
16
|
+
<% end %>
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
<%# locals: (month:, entries:) %>
|
|
2
|
-
<%# one month of the changelog (month is its first day) and its entries, newest first
|
|
1
|
+
<%# locals: (month:, entries:, announcement: nil) %>
|
|
2
|
+
<%# one month of the changelog (month is its first day) and its entries, newest first; a link to the month's
|
|
3
|
+
announcement under the heading when it has one (see shiplog_announcement_href) %>
|
|
3
4
|
<% heading_id = "shiplog-month-#{month.strftime("%Y-%m")}" %>
|
|
5
|
+
<% month_name = l(month, format: "%B %Y") %>
|
|
4
6
|
<section class="<%= shiplog_class(:month) %>" aria-labelledby="<%= heading_id %>">
|
|
5
|
-
<h2 class="<%= shiplog_class(:month_title) %>" id="<%= heading_id %>"><%=
|
|
7
|
+
<h2 class="<%= shiplog_class(:month_title) %>" id="<%= heading_id %>"><%= month_name %></h2>
|
|
8
|
+
<% if announcement && (href = shiplog_announcement_href(announcement)) %>
|
|
9
|
+
<p class="<%= shiplog_class(:month_announcement) %>">
|
|
10
|
+
<%= link_to t("shiplog.announcements.read", month: month_name, default: "Read the %{month} summary"), href, class: shiplog_class(:month_announcement_link) %>
|
|
11
|
+
</p>
|
|
12
|
+
<% end %>
|
|
6
13
|
<ul class="<%= shiplog_class(:entries) %>">
|
|
7
14
|
<%= render partial: "shiplog/changelogs/entry", collection: entries, as: :entry %>
|
|
8
15
|
</ul>
|
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
<% else %>
|
|
18
18
|
<div class="<%= shiplog_class(:months) %>">
|
|
19
19
|
<% @months.each do |month, entries| %>
|
|
20
|
-
<%= render "shiplog/changelogs/month", month:, entries: %>
|
|
20
|
+
<%= render "shiplog/changelogs/month", month:, entries:, announcement: @announcements[month] %>
|
|
21
21
|
<% end %>
|
|
22
22
|
</div>
|
|
23
23
|
<% end %>
|
|
24
24
|
<%= render "shiplog/pagination",
|
|
25
25
|
newer_path: (changelog_path(area: @area&.slug, page: (@page - 1 if @page > 2), q: @query) if @page > 1),
|
|
26
26
|
older_path: (changelog_path(area: @area&.slug, page: @page + 1, q: @query) if @older) %>
|
|
27
|
+
<% if Shiplog.newsletter? %>
|
|
28
|
+
<%= render "shiplog/newsletter/form", source: request.path %>
|
|
29
|
+
<% end %>
|
|
27
30
|
<p class="<%= shiplog_class(:footnote) %>">
|
|
28
31
|
<%= link_to t("shiplog.feed.subscribe", default: "Subscribe (Atom)"), changelog_feed_path, class: shiplog_class(:footnote_link, :feed) %>
|
|
29
32
|
</p>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# locals: (source: nil, email: nil, error: nil) %>
|
|
2
|
+
<%# The subscribe form: one email field, a honeypot (bots fill it, people never see it), a plain POST that works
|
|
3
|
+
without JavaScript. No CSRF token on purpose: the pages carrying it are cached with ETags (see
|
|
4
|
+
Shiplog::Newsletter::SubscriptionsController). `source` is recorded as the page the visitor subscribed on. %>
|
|
5
|
+
<% if Shiplog.newsletter? %>
|
|
6
|
+
<section class="<%= shiplog_class(:newsletter) %>" aria-labelledby="shiplog-newsletter-title">
|
|
7
|
+
<h2 class="<%= shiplog_class(:newsletter_title) %>" id="shiplog-newsletter-title"><%= t("shiplog.newsletter.form.title", default: "Get the monthly summary by email") %></h2>
|
|
8
|
+
<p class="<%= shiplog_class(:newsletter_hint) %>"><%= t("shiplog.newsletter.form.hint", default: "One email a month, no more. Unsubscribe any time.") %></p>
|
|
9
|
+
<% if error %>
|
|
10
|
+
<p class="<%= shiplog_class(:newsletter_notice, :error) %>" role="alert"><%= error %></p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<%= form_with url: shiplog_newsletter_subscription_path, method: :post, authenticity_token: false, class: shiplog_class(:newsletter_form), data: { turbo: false } do |form| %>
|
|
13
|
+
<%= form.hidden_field :source, value: shiplog_newsletter_source(source) %>
|
|
14
|
+
<%# the honeypot: a name no browser autofills, out of the tab order and of autofill %>
|
|
15
|
+
<div class="shiplog-newsletter__trap" aria-hidden="true">
|
|
16
|
+
<%= form.label Shiplog::Newsletter::SubscriptionsController::HONEYPOT, t("shiplog.newsletter.form.trap_label", default: "Leave this field empty"), tabindex: -1 %>
|
|
17
|
+
<%= form.text_field Shiplog::Newsletter::SubscriptionsController::HONEYPOT, tabindex: -1, autocomplete: "off", "data-1p-ignore": "", "data-lpignore": "true" %>
|
|
18
|
+
</div>
|
|
19
|
+
<%= form.label :email, t("shiplog.newsletter.form.email_label", default: "Email address"), class: shiplog_class(:newsletter_label) %>
|
|
20
|
+
<%= form.email_field :email, value: email, required: true, autocomplete: "email", maxlength: Shiplog::NewsletterSubscriber::EMAIL_MAX_LENGTH,
|
|
21
|
+
placeholder: t("shiplog.newsletter.form.email_placeholder", default: "you@example.com"), class: shiplog_class(:newsletter_input) %>
|
|
22
|
+
<%= form.button t("shiplog.newsletter.form.submit", default: "Subscribe"), type: "submit", class: shiplog_class(:newsletter_button) %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</section>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%# locals: (heading:) %>
|
|
2
|
+
<%# rendered with `render layout:`; a newsletter page: the changelog's page chrome, a heading and the block's text %>
|
|
3
|
+
<% content_for :title, t("shiplog.newsletter.title", default: "Newsletter") %>
|
|
4
|
+
<%= render layout: "shiplog/page", locals: { name: "newsletter" } do %>
|
|
5
|
+
<%= render "shiplog/page_header", title: t("shiplog.newsletter.title", default: "Newsletter"), lede: nil %>
|
|
6
|
+
<section class="<%= shiplog_class(:newsletter, :page) %>" aria-labelledby="shiplog-newsletter-heading">
|
|
7
|
+
<h2 class="<%= shiplog_class(:newsletter_title) %>" id="shiplog-newsletter-heading"><%= heading %></h2>
|
|
8
|
+
<% if notice.present? %><p class="<%= shiplog_class(:newsletter_notice) %>" role="status"><%= notice %></p><% end %>
|
|
9
|
+
<%= yield %>
|
|
10
|
+
</section>
|
|
11
|
+
<p class="<%= shiplog_class(:footnote) %>">
|
|
12
|
+
<%= link_to t("shiplog.newsletter.back", default: "← Back to the changelog"), changelog_path, class: shiplog_class(:footnote_link) %>
|
|
13
|
+
</p>
|
|
14
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# Rendered by the confirming POST: subscribed, with the way to the preferences page %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.confirm.done_heading", default: "You're subscribed") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.confirm.done_text", email: @subscriber.email, default: "The monthly summary will arrive at %{email}. You can change the language or unsubscribe any time from your preferences.") %></p>
|
|
4
|
+
<p><%= link_to t("shiplog.newsletter.preferences.heading", default: "Your subscription"), newsletter_preferences_path(@subscriber.token), class: shiplog_class(:footnote_link) %></p>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# The confirmation link's page: a button that POSTs (a GET confirms nothing, link scanners prefetch) %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.confirm.heading", default: "Confirm your subscription") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.confirm.text", email: @subscriber.email, default: "Click the button to receive the monthly summary at %{email}.") %></p>
|
|
4
|
+
<%= button_to t("shiplog.newsletter.confirm.button", default: "Confirm"), newsletter_confirmation_path(params[:token]), method: :post, class: shiplog_class(:newsletter_button), form: { data: { turbo: false } } %>
|
|
5
|
+
<% end %>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%# A link that does not verify: tampered, expired, or its subscriber was deleted (status 404) %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.invalid.heading", default: "This link is not valid") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.invalid.text", default: "It may have expired or already been used. Subscribe again from the changelog to get a new one.") %></p>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%# Rendered by "Delete my data": the row is gone %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.preferences.deleted_heading", default: "Your data was deleted") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.preferences.deleted_text", default: "Your email address is no longer stored. You can subscribe again from the changelog at any time.") %></p>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%# A subscriber's preferences (reached through the footer's token): language, unsubscribe or resubscribe, delete %>
|
|
2
|
+
<% path = newsletter_preferences_path(@subscriber.token) %>
|
|
3
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.preferences.heading", default: "Your subscription") } do %>
|
|
4
|
+
<p>
|
|
5
|
+
<%= t("shiplog.newsletter.preferences.email", default: "Email") %>: <strong><%= @subscriber.email %></strong><br>
|
|
6
|
+
<%= t("shiplog.newsletter.preferences.status", default: "Status") %>: <%= t("shiplog.newsletter.preferences.statuses.#{@subscriber.status}", default: @subscriber.status) %>
|
|
7
|
+
</p>
|
|
8
|
+
<%= form_with url: path, method: :patch, class: shiplog_class(:newsletter_form), data: { turbo: false } do |form| %>
|
|
9
|
+
<%= form.label :locale, t("shiplog.newsletter.preferences.language", default: "Language"), class: shiplog_class(:newsletter_label) %>
|
|
10
|
+
<%= form.select :locale, Shiplog::NewsletterSubscriber.allowed_locales.map { |locale| [ Shiplog::Translator.language_name(locale), locale ] },
|
|
11
|
+
{ selected: @subscriber.locale }, class: shiplog_class(:newsletter_input) %>
|
|
12
|
+
<%= form.button t("shiplog.newsletter.preferences.save", default: "Save"), type: "submit", class: shiplog_class(:newsletter_button) %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<div class="shiplog-newsletter__actions">
|
|
15
|
+
<% if @subscriber.unsubscribed? %>
|
|
16
|
+
<%= button_to t("shiplog.newsletter.preferences.resubscribe", default: "Resubscribe"), path, method: :patch, params: { subscribed: "1" }, class: shiplog_class(:newsletter_button), form: { data: { turbo: false } } %>
|
|
17
|
+
<% else %>
|
|
18
|
+
<%= button_to t("shiplog.newsletter.preferences.unsubscribe", default: "Unsubscribe"), path, method: :patch, params: { subscribed: "0" }, class: shiplog_class(:newsletter_button), form: { data: { turbo: false } } %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<%= button_to t("shiplog.newsletter.preferences.delete", default: "Delete my data"), path, method: :delete, class: shiplog_class(:newsletter_button, :secondary), form: { data: { turbo: false } } %>
|
|
21
|
+
</div>
|
|
22
|
+
<p class="<%= shiplog_class(:newsletter_hint) %>"><%= t("shiplog.newsletter.preferences.delete_help", default: "Deleting your data removes your address from our records entirely, including the record that you unsubscribed.") %></p>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%# Too many subscribe attempts from one address or one IP (status 429) %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.rate_limited.heading", default: "Too many attempts") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.rate_limited.text", default: "Please try again in an hour.") %></p>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%# "Check your inbox" after the form, the same for every address; with @error (an address that is not an email),
|
|
2
|
+
the form again (status 422) %>
|
|
3
|
+
<% if @error %>
|
|
4
|
+
<% content_for :title, t("shiplog.newsletter.title", default: "Newsletter") %>
|
|
5
|
+
<%= render layout: "shiplog/page", locals: { name: "newsletter" } do %>
|
|
6
|
+
<%= render "shiplog/page_header", title: t("shiplog.newsletter.title", default: "Newsletter"), lede: nil %>
|
|
7
|
+
<%= render "shiplog/newsletter/form", email: @email, error: @error, source: params[:source] %>
|
|
8
|
+
<p class="<%= shiplog_class(:footnote) %>">
|
|
9
|
+
<%= link_to t("shiplog.newsletter.back", default: "← Back to the changelog"), changelog_path, class: shiplog_class(:footnote_link) %>
|
|
10
|
+
</p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.sent.heading", default: "Check your inbox") } do %>
|
|
14
|
+
<p><%= t("shiplog.newsletter.sent.text", default: "We sent you a link to confirm your subscription. Open it to start receiving the monthly summary. If nothing arrives within a few minutes, look in your spam folder.") %></p>
|
|
15
|
+
<% end %>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<%# Rendered by the unsubscribing POST (the button, or a mail provider's one-click request) %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.unsubscribe.done_heading", default: "You're unsubscribed") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.unsubscribe.done_text", email: @email, default: "%{email} will not receive the monthly summary anymore.") %></p>
|
|
4
|
+
<% end %>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%# The unsubscribe link's page: a button that POSTs (a GET unsubscribes nobody, link scanners prefetch) %>
|
|
2
|
+
<%= render layout: "shiplog/newsletter/page", locals: { heading: t("shiplog.newsletter.unsubscribe.heading", default: "Unsubscribe") } do %>
|
|
3
|
+
<p><%= t("shiplog.newsletter.unsubscribe.text", email: @email, default: "Stop receiving the monthly summary at %{email}?") %></p>
|
|
4
|
+
<%= button_to t("shiplog.newsletter.unsubscribe.button", default: "Unsubscribe"), newsletter_unsubscription_path(params[:token]), method: :post, class: shiplog_class(:newsletter_button), form: { data: { turbo: false } } %>
|
|
5
|
+
<% end %>
|