Skip to content

Commit

Permalink
Make birthdate optional and less precise
Browse files Browse the repository at this point in the history
or fuzzy, as it were

- Reduces friction when signing up
- Reduces amount of personal information stored by default
- Keeps exact birthdate private in case people don’t realize that displaying their age is equivalent to displaying their birthdate, as long as the transition is recorded

We still prevent users from displaying an age that’s inconsistent with anything they’ve done with ratings, and from changing their age once set.

TODO: `asserted_adult` should be filled in for existing users just in case their birthdate is removed by site staff.
  • Loading branch information
charmander committed Mar 4, 2024
1 parent b99a63c commit 3f3131b
Show file tree
Hide file tree
Showing 27 changed files with 290 additions and 92 deletions.
23 changes: 16 additions & 7 deletions assets/js/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -1136,23 +1136,32 @@
inputElement.parentNode.classList.toggle('disabled', disable);
}

document.addEventListener('change', function (e) {
var disableId = e.target.dataset.disables;
function handleCheckState(target) {
var disableId = target.dataset.disables;

if (disableId) {
var disables = document.getElementById(disableId);
var disable = e.target.checked;
var disable = target.checked;

disableWithLabel(disables, disable);
}
});

forEach(document.querySelectorAll('[data-disables]'), function (checkbox) {
var disables = document.getElementById(checkbox.dataset.disables);
var showId = target.dataset.shows;

if (showId) {
var shows = document.getElementById(showId);

shows.style.display = target.checked ? '' : 'none';
}
}

disableWithLabel(disables, checkbox.checked);
document.addEventListener('change', function (e) {
handleCheckState(e.target);
});

forEach(document.querySelectorAll('[data-disables]'), handleCheckState);
forEach(document.querySelectorAll('[data-shows]'), handleCheckState);

(function () {
function isOtherOption(option) {
return option.hasAttribute('data-select-other');
Expand Down
16 changes: 16 additions & 0 deletions assets/js/signup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// `invalid-use-title` indicates that an element’s title is a good custom error message for when it doesn’t pass HTML form validation.
for (const element of document.getElementsByClassName('invalid-use-title')) {
const message = element.title;
element.title = '';

const updateCustomValidity = () => {
element.setCustomValidity('');

if (!element.checkValidity()) {
element.setCustomValidity(message);
}
};

element.addEventListener('change', updateCustomValidity);
updateCustomValidity();
}
1 change: 1 addition & 0 deletions assets/scss/_theme.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$content-color-lighter: #777;
32 changes: 26 additions & 6 deletions assets/scss/components/_date-picker.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
.form-date-day, .form-date-year {
width: 25%;
float: left;
@import '../theme';

.form-date {
display: flex;
gap: 4px;
}

.form-date > * {
// `!important`s here override terrible global `label` style that needs refactoring
display: flex !important;
flex-direction: column;
padding: 0 !important;
}

.form-date > * > .sublabel {
color: $content-color-lighter;
padding-top: 0.35em;
font-size: 12px;
font-size: 0.75rem;
font-weight: 400;
}

.form-date-month {
width: 46%;
float: left;
padding: 0 2%;
flex: 1;
max-width: 12em;
}

.form-date > .form-date-year > input {
width: 6em;
}
1 change: 0 additions & 1 deletion assets/scss/signup.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import 'components/date-picker';
@import 'components/password-meter';

#signup-terms {
Expand Down
29 changes: 15 additions & 14 deletions assets/scss/site.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@import 'theme';
@import 'reset';
@import 'components/date-picker';
@import 'components/option';
@import 'components/text-post-list';
@import 'components/user-tabs';
Expand Down Expand Up @@ -135,8 +137,6 @@ a.more span, #header-messages a, .page-title a {
color: #943522;
}

$content-color-lighter: #777;

.content .color-lighter {
color: $content-color-lighter;
}
Expand Down Expand Up @@ -751,16 +751,28 @@ dl + h4, #uc-info {
opacity: 0.6;
}

$input-background: #e4e4e4;

.input {
box-sizing: border-box;
padding: 0 0.5em;
border-style: solid;
border-width: 1px;
border-color: #aaa #bbb #ccc;
border-radius: 0;
background-color: #e4e4e4;
background-color: $input-background;
box-shadow: 0 4px 0 #336979, inset 0 1px 2px #aaa;
transition: all 0.15s linear;

&:hover,
&:focus {
background-color: #eee;
}

&:disabled {
background-color: $input-background;
box-shadow: 0 4px 0 #aaa;
}
}

.input::placeholder {
Expand All @@ -778,10 +790,6 @@ select.input {
line-height: 1.75rem;
}

.input:hover, .input:focus {
background-color: #eee;
}

.input:focus {
box-shadow: 0 4px 0 #069bc0;
}
Expand Down Expand Up @@ -1639,13 +1647,6 @@ $tag-reject-deemphasize-size: 11px;
font-weight: 700;
}

.form label.color-lighter:not(.input-checkbox) {
padding-top: 0.35em;
font-size: 12px;
font-size: 0.75rem;
font-weight: 400;
}

.optional-indicator {
font-weight: normal;
color: $content-color-lighter;
Expand Down
1 change: 1 addition & 0 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ const main = async () => {
target: 'es6',
banner: {},
}),
esbuildFile('js/signup.js', 'js/signup.js', touch, PRIVATE_FIELDS_ESM),
copyStaticFiles('img/help', touch),
copyUnversionedStaticFile('opensearch.xml', touch),
copyImages,
Expand Down
34 changes: 34 additions & 0 deletions libweasyl/alembic/versions/57171ee9e989_make_birthdate_optional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Make birthdate optional
Revision ID: 57171ee9e989
Revises: 9a41d4fa38ba
Create Date: 2023-12-22 00:15:53.754117
"""

# revision identifiers, used by Alembic.
revision = '57171ee9e989'
down_revision = '9a41d4fa38ba'

from alembic import op
import sqlalchemy as sa


def upgrade():
op.alter_column('logincreate', 'birthday',
existing_type=sa.INTEGER(),
nullable=True)
op.add_column('userinfo', sa.Column('asserted_adult', sa.Boolean(), server_default='f', nullable=False))
op.alter_column('userinfo', 'birthday',
existing_type=sa.INTEGER(),
nullable=True)


def downgrade():
op.alter_column('userinfo', 'birthday',
existing_type=sa.INTEGER(),
nullable=False)
op.drop_column('userinfo', 'asserted_adult')
op.alter_column('logincreate', 'birthday',
existing_type=sa.INTEGER(),
nullable=False)
5 changes: 3 additions & 2 deletions libweasyl/models/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ def default_fkey(*args, **kwargs):
Column('login_name', String(length=40), nullable=False, unique=True),
Column('hashpass', String(length=100), nullable=False),
Column('email', String(length=100), nullable=False, unique=True),
Column('birthday', WeasylTimestampColumn(), nullable=False),
Column('birthday', WeasylTimestampColumn(), nullable=True),
Column('created_at', TIMESTAMP(timezone=True), nullable=False, server_default=func.now()),
# Used to determine if a record is invalid for purposes of plausible deniability of email addresses
# AKA, create a logincreate entry if an in-use email address is provided, thus preserving the effect of
Expand Down Expand Up @@ -884,7 +884,8 @@ def _tag_suggestion_feedback_table(content_table, id_column):
userinfo = Table(
'userinfo', metadata,
Column('userid', Integer(), primary_key=True, nullable=False),
Column('birthday', WeasylTimestampColumn(), nullable=False),
Column('birthday', WeasylTimestampColumn(), nullable=True),
Column('asserted_adult', Boolean(), nullable=False, server_default='f'),
Column('gender', String(length=100), nullable=False, server_default=''),
Column('country', String(length=50), nullable=False, server_default=''),
default_fkey(['userid'], ['login.userid'], name='userinfo_userid_fkey'),
Expand Down
3 changes: 3 additions & 0 deletions libweasyl/ratings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@ def __hash__(self):


def get_ratings_for_age(age):
if age is None:
return ALL_RATINGS

Check warning on line 57 in libweasyl/ratings.py

View check run for this annotation

Codecov / codecov/patch

libweasyl/ratings.py#L57

Added line #L57 was not covered by tests

age = max(0, age)
return [rating for rating in ALL_RATINGS if rating.minimum_age <= age]
4 changes: 4 additions & 0 deletions weasyl/character.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def create(userid, character, friends, tags, thumbfile, submitfile):
elif not character.rating:
raise WeasylError("ratingInvalid")
profile.check_user_rating_allowed(userid, character.rating)
if character.rating.minimum_age:
profile.assert_adult(userid)

Check warning on line 47 in weasyl/character.py

View check run for this annotation

Codecov / codecov/patch

weasyl/character.py#L47

Added line #L47 was not covered by tests

# Write temporary thumbnail file
if thumbsize:
Expand Down Expand Up @@ -372,6 +374,8 @@ def edit(userid, character, friends_only):

if userid == query.userid:
profile.check_user_rating_allowed(userid, character.rating)
if character.rating.minimum_age:
profile.assert_adult(userid)

Check warning on line 378 in weasyl/character.py

View check run for this annotation

Codecov / codecov/patch

weasyl/character.py#L378

Added line #L378 was not covered by tests

if friends_only:
welcome.character_remove(character.charid)
Expand Down
4 changes: 4 additions & 0 deletions weasyl/errorcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
"fully processed.")

error_messages = {
"birthdayInconsistentWithRating": (
"You’ve already confirmed that you were 18 or older when setting rating preferences or a post’s rating; Weasyl won’t show an age inconsistent with that on your profile."),
"birthdayInconsistentWithTerms": (
"You’ve already confirmed that you were 13 or older when signing up; Weasyl won’t show an age inconsistent with that on your profile."),
"birthdayInsufficient": (
"Your date of birth indicates that you are not allowed to set your content rating settings to the "
"level you entered. Please choose a lower rating level."),
Expand Down
4 changes: 4 additions & 0 deletions weasyl/journal.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def create(userid, journal, friends_only=False, tags=None):
elif not journal.rating:
raise WeasylError("ratingInvalid")
profile.check_user_rating_allowed(userid, journal.rating)
if journal.rating.minimum_age:
profile.assert_adult(userid)

Check warning on line 35 in weasyl/journal.py

View check run for this annotation

Codecov / codecov/patch

weasyl/journal.py#L35

Added line #L35 was not covered by tests

# Create journal
jo = d.meta.tables["journal"]
Expand Down Expand Up @@ -280,6 +282,8 @@ def edit(userid, journal, friends_only=False):

if userid == query.userid:
profile.check_user_rating_allowed(userid, journal.rating)
if journal.rating.minimum_age:
profile.assert_adult(userid)

Check warning on line 286 in weasyl/journal.py

View check run for this annotation

Codecov / codecov/patch

weasyl/journal.py#L286

Added line #L286 was not covered by tests

if friends_only:
welcome.journal_remove(journal.journalid)
Expand Down
11 changes: 9 additions & 2 deletions weasyl/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,17 +207,24 @@ def create(form):

email = emailer.normalize_address(form.email)

# TODO: remove birth date check after checkbox-only form has been deployed for a while
password = form.password
if form.day and form.month and form.year:
try:
birthday = arrow.Arrow(int(form.year), int(form.month), int(form.day))
except ValueError:
raise WeasylError("birthdayInvalid")

if d.age_in_years(birthday) < 13:
raise WeasylError("birthdayInvalid")
else:
birthday = None

if "age" in form and form.age != "13+":
raise WeasylError("birthdayInvalid")

Check warning on line 224 in weasyl/login.py

View check run for this annotation

Codecov / codecov/patch

weasyl/login.py#L224

Added line #L224 was not covered by tests

# Check invalid form data
if birthday is None or d.age_in_years(birthday) < 13:
if birthday is None and "age" not in form:
raise WeasylError("birthdayInvalid")
if not password_secure(password):
raise WeasylError("passwordInsecure")
Expand Down Expand Up @@ -261,7 +268,7 @@ def create(form):
"login_name": sysname,
"hashpass": passhash(password),
"email": token,
"birthday": arrow.utcnow(),
"birthday": None,
"invalid": True,
# So we have a way for admins to determine which email address collided in the View Pending Accounts Page
"invalid_email_addr": email,
Expand Down
Loading

0 comments on commit 3f3131b

Please sign in to comment.