mirror of https://github.com/mastodon/mastodon
Add effective date to terms of service
This commit is contained in:
parent
b1692628de
commit
7786890326
|
@ -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",
|
||||
|
|
|
@ -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(effective_date: :desc) }
|
||||
scope :live, -> { published.where('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
|
||||
|
|
|
@ -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
|
||||
|
||||
.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
|
||||
|
|
|
@ -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))
|
||||
%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: @terms_of_service.effective_date) %>
|
||||
|
||||
=> <%= terms_of_service_url %>
|
||||
=> <%= terms_of_service_version_url(date: @terms_of_service.effective_date) %>
|
||||
|
||||
<%= t('user_mailer.terms_of_service_changed.changelog') %>
|
||||
|
||||
|
|
|
@ -49,6 +49,10 @@ en:
|
|||
attributes:
|
||||
reblog:
|
||||
taken: of post already exists
|
||||
terms_of_service:
|
||||
attributes:
|
||||
effective_date:
|
||||
too_soon: is too soon, must be later than %{date}
|
||||
user:
|
||||
attributes:
|
||||
email:
|
||||
|
|
|
@ -939,6 +939,7 @@ en:
|
|||
chance_to_review_html: "<strong>The generated terms of service will not be published automatically.</strong> You will have a chance to review the results. Please fill in the necessary details to proceed."
|
||||
explanation_html: The terms of service template provided is for informational purposes only, and should not be construed as legal advice on any subject matter. Please consult with your own legal counsel on your situation and specific legal questions you have.
|
||||
title: Terms of Service Setup
|
||||
going_live_on_html: Live, effective %{date}
|
||||
history: History
|
||||
live: Live
|
||||
no_history: There are no recorded changes of the terms of service yet.
|
||||
|
@ -1937,8 +1938,8 @@ en:
|
|||
terms_of_service_changed:
|
||||
agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account.
|
||||
changelog: 'At a glance, here is what this update means for you:'
|
||||
description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. We encourage you to review the updated terms in full here:'
|
||||
description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
|
||||
description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. These updates will take effect %{date}. We encourage you to review the updated terms in full here:'
|
||||
description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. These updates will take effect <strong>%{date}</strong>. We encourage you to review the <a href="%{path}" target="_blank">updated terms in full here</a>.
|
||||
sign_off: The %{domain} team
|
||||
subject: Updates to our terms of service
|
||||
subtitle: The terms of service of %{domain} are changing
|
||||
|
|
|
@ -132,6 +132,7 @@ en:
|
|||
name: You can only change the casing of the letters, for example, to make it more readable
|
||||
terms_of_service:
|
||||
changelog: Can be structured with Markdown syntax.
|
||||
effective_date: A reasonable timeframe can range anywhere from 10 to 30 days from the date you notify your users.
|
||||
text: Can be structured with Markdown syntax.
|
||||
terms_of_service_generator:
|
||||
admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests.
|
||||
|
@ -333,6 +334,7 @@ en:
|
|||
usable: Allow posts to use this hashtag locally
|
||||
terms_of_service:
|
||||
changelog: What's changed?
|
||||
effective_date: Effective date
|
||||
text: Terms of Service
|
||||
terms_of_service_generator:
|
||||
admin_email: Email address for legal notices
|
||||
|
|
|
@ -204,7 +204,8 @@ Rails.application.routes.draw do
|
|||
|
||||
get '/privacy-policy', to: 'privacy#show', as: :privacy_policy
|
||||
get '/terms-of-service', to: 'terms_of_service#show', as: :terms_of_service
|
||||
get '/terms', to: redirect('/terms-of-service')
|
||||
get '/terms-of-service/:date', to: 'terms_of_service#show', as: :terms_of_service_version
|
||||
get '/terms', to: redirect('/terms-of-service')
|
||||
|
||||
match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false
|
||||
match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false
|
||||
|
|
|
@ -121,6 +121,8 @@ namespace :api, format: false do
|
|||
resource :translation_languages, only: [:show]
|
||||
resource :languages, only: [:show]
|
||||
resource :activity, only: [:show], controller: :activity
|
||||
|
||||
get '/terms_of_service/:date', to: 'terms_of_services#show'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddEffectiveDateToTermsOfServices < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :terms_of_services, :effective_date, :date
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_01_29_144813) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_02_24_144617) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
|
@ -1110,6 +1110,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_29_144813) do
|
|||
t.datetime "notification_sent_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.date "effective_date"
|
||||
end
|
||||
|
||||
create_table "tombstones", force: :cascade do |t|
|
||||
|
|
|
@ -5,4 +5,5 @@ Fabricator(:terms_of_service) do
|
|||
changelog { Faker::Lorem.paragraph }
|
||||
published_at { Time.zone.now }
|
||||
notification_sent_at { Time.zone.now }
|
||||
effective_date { Time.zone.tomorrow }
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue