Add effective date to terms of service (#33993)
This commit is contained in:
parent
84164270c6
commit
cadda2f957
67 changed files with 201 additions and 126 deletions
|
@ -23,7 +23,7 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
|||
private
|
||||
|
||||
def set_terms_of_service
|
||||
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text)
|
||||
@terms_of_service = TermsOfService.draft.first || TermsOfService.new(text: current_terms_of_service&.text, effective_date: 10.days.from_now)
|
||||
end
|
||||
|
||||
def current_terms_of_service
|
||||
|
@ -32,6 +32,6 @@ class Admin::TermsOfService::DraftsController < Admin::BaseController
|
|||
|
||||
def resource_params
|
||||
params
|
||||
.expect(terms_of_service: [:text, :changelog])
|
||||
.expect(terms_of_service: [:text, :changelog, :effective_date])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
class Admin::TermsOfServiceController < Admin::BaseController
|
||||
def index
|
||||
authorize :terms_of_service, :index?
|
||||
@terms_of_service = TermsOfService.live.first
|
||||
@terms_of_service = TermsOfService.published.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,12 +5,18 @@ class Api::V1::Instances::TermsOfServicesController < Api::V1::Instances::BaseCo
|
|||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render json: @terms_of_service, serializer: REST::PrivacyPolicySerializer
|
||||
render json: @terms_of_service, serializer: REST::TermsOfServiceSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_terms_of_service
|
||||
@terms_of_service = TermsOfService.live.first!
|
||||
@terms_of_service = begin
|
||||
if params[:date].present?
|
||||
TermsOfService.published.find_by!(effective_date: params[:date])
|
||||
else
|
||||
TermsOfService.live.first || TermsOfService.published.first! # For the case when none of the published terms have become effective yet
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,8 +4,12 @@ import type {
|
|||
ApiPrivacyPolicyJSON,
|
||||
} from 'mastodon/api_types/instance';
|
||||
|
||||
export const apiGetTermsOfService = () =>
|
||||
apiRequestGet<ApiTermsOfServiceJSON>('v1/instance/terms_of_service');
|
||||
export const apiGetTermsOfService = (version?: string) =>
|
||||
apiRequestGet<ApiTermsOfServiceJSON>(
|
||||
version
|
||||
? `v1/instance/terms_of_service/${version}`
|
||||
: 'v1/instance/terms_of_service',
|
||||
);
|
||||
|
||||
export const apiGetPrivacyPolicy = () =>
|
||||
apiRequestGet<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export interface ApiTermsOfServiceJSON {
|
||||
updated_at: string;
|
||||
effective_date: string;
|
||||
effective: boolean;
|
||||
succeeded_by: string | null;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,26 +8,31 @@ import {
|
|||
} from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { apiGetTermsOfService } from 'mastodon/api/instance';
|
||||
import type { ApiTermsOfServiceJSON } from 'mastodon/api_types/instance';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'terms_of_service.title', defaultMessage: 'Terms of Service' },
|
||||
});
|
||||
|
||||
interface Params {
|
||||
date?: string;
|
||||
}
|
||||
|
||||
const TermsOfService: React.FC<{
|
||||
multiColumn: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const intl = useIntl();
|
||||
const { date } = useParams<Params>();
|
||||
const [response, setResponse] = useState<ApiTermsOfServiceJSON>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
apiGetTermsOfService()
|
||||
apiGetTermsOfService(date)
|
||||
.then((data) => {
|
||||
setResponse(data);
|
||||
setLoading(false);
|
||||
|
@ -36,7 +41,7 @@ const TermsOfService: React.FC<{
|
|||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
}, [date]);
|
||||
|
||||
if (!loading && !response) {
|
||||
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
|
||||
|
@ -55,23 +60,60 @@ const TermsOfService: React.FC<{
|
|||
defaultMessage='Terms of Service'
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='privacy_policy.last_updated'
|
||||
defaultMessage='Last updated {date}'
|
||||
values={{
|
||||
date: loading ? (
|
||||
<Skeleton width='10ch' />
|
||||
) : (
|
||||
<FormattedDate
|
||||
value={response?.updated_at}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
<p className='prose'>
|
||||
{response?.effective ? (
|
||||
<FormattedMessage
|
||||
id='privacy_policy.last_updated'
|
||||
defaultMessage='Last updated {date}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={response.effective_date}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='terms_of_service.effective_as_of'
|
||||
defaultMessage='Effective as of {date}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={response?.effective_date}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{response?.succeeded_by && (
|
||||
<>
|
||||
{' · '}
|
||||
<Link to={`/terms-of-service/${response.succeeded_by}`}>
|
||||
<FormattedMessage
|
||||
id='terms_of_service.upcoming_changes_on'
|
||||
defaultMessage='Upcoming changes on {date}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={response.succeeded_by}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||
<WrappedRoute path='/about' component={About} content={children} />
|
||||
<WrappedRoute path='/privacy-policy' component={PrivacyPolicy} content={children} />
|
||||
<WrappedRoute path='/terms-of-service' component={TermsOfService} content={children} />
|
||||
<WrappedRoute path='/terms-of-service/:date?' component={TermsOfService} content={children} />
|
||||
|
||||
<WrappedRoute path={['/home', '/timelines/home']} component={HomeTimeline} content={children} />
|
||||
<Redirect from='/timelines/public' to='/public' exact />
|
||||
|
|
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"terms_of_service.effective_as_of": "Effective as of {date}",
|
||||
"terms_of_service.title": "Terms of Service",
|
||||
"terms_of_service.upcoming_changes_on": "Upcoming changes on {date}",
|
||||
"time_remaining.days": "{number, plural, one {# day} other {# days}} left",
|
||||
"time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left",
|
||||
"time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
|
||||
|
|
|
@ -1989,6 +1989,11 @@ a.sparkline {
|
|||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -340,10 +340,17 @@ code {
|
|||
columns: unset;
|
||||
}
|
||||
|
||||
.input.datetime .label_input select {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
flex: 0;
|
||||
.input.datetime .label_input,
|
||||
.input.date .label_input {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
select {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
flex: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input.select.select--languages {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# changelog :text default(""), not null
|
||||
# effective_date :date
|
||||
# notification_sent_at :datetime
|
||||
# published_at :datetime
|
||||
# text :text default(""), not null
|
||||
|
@ -13,17 +14,27 @@
|
|||
# updated_at :datetime not null
|
||||
#
|
||||
class TermsOfService < ApplicationRecord
|
||||
scope :published, -> { where.not(published_at: nil).order(published_at: :desc) }
|
||||
scope :live, -> { published.limit(1) }
|
||||
scope :published, -> { where.not(published_at: nil).order(Arel.sql('coalesce(effective_date, published_at) DESC')) }
|
||||
scope :live, -> { published.where('effective_date IS NULL OR effective_date < now()').limit(1) }
|
||||
scope :draft, -> { where(published_at: nil).order(id: :desc).limit(1) }
|
||||
|
||||
validates :text, presence: true
|
||||
validates :changelog, presence: true, if: -> { published? }
|
||||
validates :changelog, :effective_date, presence: true, if: -> { published? }
|
||||
|
||||
validate :effective_date_cannot_be_in_the_past
|
||||
|
||||
def published?
|
||||
published_at.present?
|
||||
end
|
||||
|
||||
def effective?
|
||||
published? && effective_date&.past?
|
||||
end
|
||||
|
||||
def succeeded_by
|
||||
TermsOfService.published.where(effective_date: (effective_date..)).where.not(id: id).first
|
||||
end
|
||||
|
||||
def notification_sent?
|
||||
notification_sent_at.present?
|
||||
end
|
||||
|
@ -31,4 +42,14 @@ class TermsOfService < ApplicationRecord
|
|||
def scope_for_notification
|
||||
User.confirmed.joins(:account).merge(Account.without_suspended).where(created_at: (..published_at))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def effective_date_cannot_be_in_the_past
|
||||
return if effective_date.blank?
|
||||
|
||||
min_date = TermsOfService.live.pick(:effective_date) || Time.zone.today
|
||||
|
||||
errors.add(:effective_date, :too_soon, date: min_date) if effective_date < min_date
|
||||
end
|
||||
end
|
||||
|
|
27
app/serializers/rest/terms_of_service_serializer.rb
Normal file
27
app/serializers/rest/terms_of_service_serializer.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::TermsOfServiceSerializer < ActiveModel::Serializer
|
||||
attributes :effective_date, :effective, :content, :succeeded_by
|
||||
|
||||
def effective_date
|
||||
object.effective_date.iso8601
|
||||
end
|
||||
|
||||
def effective
|
||||
object.effective?
|
||||
end
|
||||
|
||||
def succeeded_by
|
||||
object.succeeded_by&.effective_date&.iso8601
|
||||
end
|
||||
|
||||
def content
|
||||
markdown.render(format(object.text, domain: Rails.configuration.x.local_domain))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def markdown
|
||||
@markdown ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, escape_html: true, no_images: true)
|
||||
end
|
||||
end
|
|
@ -14,6 +14,9 @@
|
|||
.fields-group
|
||||
= form.input :changelog, wrapper: :with_block_label, input_html: { rows: 8 }
|
||||
|
||||
.fields-group
|
||||
= form.input :effective_date, wrapper: :with_block_label, as: :date, start_year: Time.zone.today.year
|
||||
|
||||
.actions
|
||||
= form.button :button, t('admin.terms_of_service.save_draft'), type: :submit, name: :action_type, value: :save_draft, class: 'button button-secondary'
|
||||
= form.button :button, t('admin.terms_of_service.publish'), type: :submit, name: :action_type, value: :publish
|
||||
|
|
|
@ -12,5 +12,9 @@
|
|||
- @terms_of_service.each do |terms_of_service|
|
||||
%li
|
||||
.admin__terms-of-service__history__item
|
||||
%h5= l(terms_of_service.published_at)
|
||||
%h5
|
||||
- if terms_of_service.effective_date.present?
|
||||
= link_to l(terms_of_service.published_at), terms_of_service_version_path(date: terms_of_service.effective_date)
|
||||
- else
|
||||
= l(terms_of_service.published_at)
|
||||
.prose= markdown(terms_of_service.changelog)
|
||||
|
|
|
@ -10,7 +10,11 @@
|
|||
.admin__terms-of-service__container__header
|
||||
.dot-indicator.success
|
||||
.dot-indicator__indicator
|
||||
%span= t('admin.terms_of_service.live')
|
||||
%span
|
||||
- if @terms_of_service.effective?
|
||||
= t('admin.terms_of_service.live')
|
||||
- else
|
||||
= t('admin.terms_of_service.going_live_on_html', date: tag.time(l(@terms_of_service.effective_date), class: 'formatted', date: @terms_of_service.effective_date.iso8601))
|
||||
·
|
||||
%span
|
||||
= t('admin.terms_of_service.published_on_html', date: tag.time(l(@terms_of_service.published_at.to_date), class: 'formatted', date: @terms_of_service.published_at.to_date.iso8601))
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||
%tr
|
||||
%td.email-inner-card-td.email-prose
|
||||
%p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_url, domain: site_hostname)
|
||||
%p= t('user_mailer.terms_of_service_changed.description_html', path: terms_of_service_version_url(date: @terms_of_service.effective_date), domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today))
|
||||
%p
|
||||
%strong= t('user_mailer.terms_of_service_changed.changelog')
|
||||
= markdown(@terms_of_service.changelog)
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
===
|
||||
|
||||
<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname) %>
|
||||
<%= t('user_mailer.terms_of_service_changed.description', domain: site_hostname, date: l(@terms_of_service.effective_date || Time.zone.today)) %>
|
||||
|
||||
=> <%= terms_of_service_url %>
|
||||
=> <%= terms_of_service_version_url(date: @terms_of_service.effective_date) %>
|
||||
|
||||
<%= t('user_mailer.terms_of_service_changed.changelog') %>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue