Browse Source

Adds Reading time functionality to Link texts, and adds a setting to update user bio to adjust their reading speed. New column was added to the database for words_per_minute. By default, this value will be set to 0 and only after a user updates their reading speed will reading time be displayed in the link information box.:

merge-requests/138/head
NubWizard 2 years ago
parent
commit
9b0dd9849b
  1. 34
      tildes/a
  2. 24
      tildes/alembic/versions/132ed4b3e988_words_per_minute.py
  3. 1
      tildes/consumers/topic_metadata_generator.py
  4. 1
      tildes/development.ini
  5. 1
      tildes/scripts/clean_private_data.py
  6. 63
      tildes/select user
  7. 6
      tildes/static/js/scripts.js
  8. 8
      tildes/tildes/enums.py
  9. 3
      tildes/tildes/models/user/user.py
  10. 5
      tildes/tildes/routes.py
  11. 5
      tildes/tildes/schemas/user.py
  12. 4
      tildes/tildes/templates/settings.jinja2
  13. 32
      tildes/tildes/templates/settings_wpm.jinja2
  14. 43
      tildes/tildes/templates/topic.jinja2
  15. 4
      tildes/tildes/views/api/web/exceptions.py
  16. 21
      tildes/tildes/views/api/web/user.py
  17. 6
      tildes/tildes/views/settings.py
  18. 6
      tildes/tildes/views/topic.py

34
tildes/a

@ -0,0 +1,34 @@
List of relations
Schema | Name | Type | Owner
--------+-----------------------+-------+--------
public | alembic_version | table | tildes
public | comment_bookmarks | table | tildes
public | comment_labels | table | tildes
public | comment_notifications | table | tildes
public | comment_votes | table | tildes
public | comments | table | tildes
public | financials | table | tildes
public | group_scripts | table | tildes
public | group_stats | table | tildes
public | group_subscriptions | table | tildes
public | group_wiki_pages | table | tildes
public | groups | table | tildes
public | log | table | tildes
public | log_comments | table | tildes
public | log_topics | table | tildes
public | message_conversations | table | tildes
public | message_replies | table | tildes
public | scraper_results | table | tildes
public | topic_bookmarks | table | tildes
public | topic_ignores | table | tildes
public | topic_schedule | table | tildes
public | topic_visits | table | tildes
public | topic_votes | table | tildes
public | topics | table | tildes
public | user_group_settings | table | tildes
public | user_invite_codes | table | tildes
public | user_permissions | table | tildes
public | user_rate_limit | table | tildes
public | users | table | tildes
(29 rows)

24
tildes/alembic/versions/132ed4b3e988_words_per_minute.py

@ -0,0 +1,24 @@
"""Words per Minute
Revision ID: 132ed4b3e988
Revises: 55f4c1f951d5
Create Date: 2023-06-07 23:44:32.533936
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "132ed4b3e988"
down_revision = "55f4c1f951d5"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("users", sa.Column("words_per_minute", sa.Integer(), nullable=True))
def downgrade():
op.drop_column("users", "words_per_minute")

1
tildes/consumers/topic_metadata_generator.py

@ -2,7 +2,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Consumer that generates content_metadata for topics."""
from collections.abc import Sequence
from typing import Any
from ipaddress import ip_address

1
tildes/development.ini

@ -43,3 +43,4 @@ webassets.base_dir = %(here)s/static
webassets.base_url = /
webassets.cache = false
webassets.manifest = json

1
tildes/scripts/clean_private_data.py

@ -207,6 +207,7 @@ class DataCleaner:
"last_exemplary_label_time": DEFAULT,
"_bio_markdown": DEFAULT,
"bio_rendered_html": DEFAULT,
"words_per_minute": DEFAULT,
},
synchronize_session=False,
)

63
tildes/select user

@ -0,0 +1,63 @@
Available help:
ABORT CREATE FOREIGN DATA WRAPPER DROP ROUTINE
ALTER AGGREGATE CREATE FOREIGN TABLE DROP RULE
ALTER COLLATION CREATE FUNCTION DROP SCHEMA
ALTER CONVERSION CREATE GROUP DROP SEQUENCE
ALTER DATABASE CREATE INDEX DROP SERVER
ALTER DEFAULT PRIVILEGES CREATE LANGUAGE DROP STATISTICS
ALTER DOMAIN CREATE MATERIALIZED VIEW DROP SUBSCRIPTION
ALTER EVENT TRIGGER CREATE OPERATOR DROP TABLE
ALTER EXTENSION CREATE OPERATOR CLASS DROP TABLESPACE
ALTER FOREIGN DATA WRAPPER CREATE OPERATOR FAMILY DROP TEXT SEARCH CONFIGURATION
ALTER FOREIGN TABLE CREATE POLICY DROP TEXT SEARCH DICTIONARY
ALTER FUNCTION CREATE PROCEDURE DROP TEXT SEARCH PARSER
ALTER GROUP CREATE PUBLICATION DROP TEXT SEARCH TEMPLATE
ALTER INDEX CREATE ROLE DROP TRANSFORM
ALTER LANGUAGE CREATE RULE DROP TRIGGER
ALTER LARGE OBJECT CREATE SCHEMA DROP TYPE
ALTER MATERIALIZED VIEW CREATE SEQUENCE DROP USER
ALTER OPERATOR CREATE SERVER DROP USER MAPPING
ALTER OPERATOR CLASS CREATE STATISTICS DROP VIEW
ALTER OPERATOR FAMILY CREATE SUBSCRIPTION END
ALTER POLICY CREATE TABLE EXECUTE
ALTER PROCEDURE CREATE TABLE AS EXPLAIN
ALTER PUBLICATION CREATE TABLESPACE FETCH
ALTER ROLE CREATE TEXT SEARCH CONFIGURATION GRANT
ALTER ROUTINE CREATE TEXT SEARCH DICTIONARY IMPORT FOREIGN SCHEMA
ALTER RULE CREATE TEXT SEARCH PARSER INSERT
ALTER SCHEMA CREATE TEXT SEARCH TEMPLATE LISTEN
ALTER SEQUENCE CREATE TRANSFORM LOAD
ALTER SERVER CREATE TRIGGER LOCK
ALTER STATISTICS CREATE TYPE MOVE
ALTER SUBSCRIPTION CREATE USER NOTIFY
ALTER SYSTEM CREATE USER MAPPING PREPARE
ALTER TABLE CREATE VIEW PREPARE TRANSACTION
ALTER TABLESPACE DEALLOCATE REASSIGN OWNED
ALTER TEXT SEARCH CONFIGURATION DECLARE REFRESH MATERIALIZED VIEW
ALTER TEXT SEARCH DICTIONARY DELETE REINDEX
ALTER TEXT SEARCH PARSER DISCARD RELEASE SAVEPOINT
ALTER TEXT SEARCH TEMPLATE DO RESET
ALTER TRIGGER DROP ACCESS METHOD REVOKE
ALTER TYPE DROP AGGREGATE ROLLBACK
ALTER USER DROP CAST ROLLBACK PREPARED
ALTER USER MAPPING DROP COLLATION ROLLBACK TO SAVEPOINT
ALTER VIEW DROP CONVERSION SAVEPOINT
ANALYZE DROP DATABASE SECURITY LABEL
BEGIN DROP DOMAIN SELECT
CALL DROP EVENT TRIGGER SELECT INTO
CHECKPOINT DROP EXTENSION SET
CLOSE DROP FOREIGN DATA WRAPPER SET CONSTRAINTS
CLUSTER DROP FOREIGN TABLE SET ROLE
COMMENT DROP FUNCTION SET SESSION AUTHORIZATION
COMMIT DROP GROUP SET TRANSACTION
COMMIT PREPARED DROP INDEX SHOW
COPY DROP LANGUAGE START TRANSACTION
CREATE ACCESS METHOD DROP MATERIALIZED VIEW TABLE
CREATE AGGREGATE DROP OPERATOR TRUNCATE
CREATE CAST DROP OPERATOR CLASS UNLISTEN
CREATE COLLATION DROP OPERATOR FAMILY UPDATE
CREATE CONVERSION DROP OWNED VACUUM
CREATE DATABASE DROP POLICY VALUES
CREATE DOMAIN DROP PROCEDURE WITH
CREATE EVENT TRIGGER DROP PUBLICATION
CREATE EXTENSION DROP ROLE

6
tildes/static/js/scripts.js

@ -84,9 +84,9 @@ $(function() {
}
// check if the response came back as HTML (unhandled error of some sort)
if (errorText.lastIndexOf("<html>", 500) !== -1) {
errorText = "Unknown error";
}
//if (errorText.lastIndexOf("<html>", 500) !== -1) {
// errorText = "Unknown error";
//}
$statusElement.addClass("text-error").text(errorText);
}

8
tildes/tildes/enums.py

@ -94,6 +94,7 @@ class ContentMetadataFields(enum.Enum):
PUBLISHED = enum.auto()
TITLE = enum.auto()
WORD_COUNT = enum.auto()
TIME_TO_READ = enum.auto()
@property
def key(self) -> str:
@ -112,10 +113,10 @@ class ContentMetadataFields(enum.Enum):
) -> list["ContentMetadataFields"]:
"""Return a list of fields to display for detail about a particular type."""
if content_type is TopicContentType.ARTICLE:
return [cls.WORD_COUNT, cls.PUBLISHED]
return [cls.WORD_COUNT, cls.TIME_TO_READ, cls.PUBLISHED]
if content_type is TopicContentType.TEXT:
return [cls.WORD_COUNT]
return [cls.WORD_COUNT, cls.TIME_TO_READ]
if content_type is TopicContentType.VIDEO:
return [cls.DURATION, cls.PUBLISHED]
@ -155,6 +156,9 @@ class ContentMetadataFields(enum.Enum):
return f"{word_count} words"
if self.name == "TIME_TO_READ":
return f"{value:.1f} minutes"
return str(value)

3
tildes/tildes/models/user/user.py

@ -135,6 +135,9 @@ class User(DatabaseModel):
)
comment_label_weight: Optional[float] = Column(REAL)
last_exemplary_label_time: Optional[datetime] = Column(TIMESTAMP(timezone=True))
words_per_minute: int = Column(Integer, default=0)
_bio_markdown: str = deferred(
Column(
"bio_markdown",

5
tildes/tildes/routes.py

@ -105,6 +105,9 @@ def includeme(config: Configurator) -> None:
config.add_route(
"settings_theme_previews", "/theme_previews", factory=LoggedInFactory
)
config.add_route("settings_wpm", "/wpm", factory=LoggedInFactory)
# config.add_route("user_wpm", "/user/{username}/settings/wpm", factory=user_by_username)
# config.add_route("patch_change_wpm", "/settings/wpm")
config.add_route("bookmarks", "/bookmarks", factory=LoggedInFactory)
config.add_route("ignored_topics", "/ignored_topics", factory=LoggedInFactory)
@ -183,6 +186,8 @@ def add_intercooler_routes(config: Configurator) -> None:
add_ic_route("markdown_preview", "/markdown_preview", factory=LoggedInFactory)
# add_ic_route("user_wpm", "/settings/wpm", factory=user_by_username)
class LoggedInFactory:
"""Simple class to use as `factory` to restrict routes to logged-in users.

5
tildes/tildes/schemas/user.py

@ -8,8 +8,8 @@ from typing import Any
from marshmallow import post_dump, pre_load, Schema, validates, validates_schema
from marshmallow.exceptions import ValidationError
from marshmallow.fields import DateTime, Email, String
from marshmallow.validate import Length, Regexp
from marshmallow.fields import DateTime, Email, String, Integer
from marshmallow.validate import Length, Regexp, Range
from tildes.lib.password import is_breached_password
from tildes.schemas.fields import Markdown
@ -58,6 +58,7 @@ class UserSchema(Schema):
email_address_note = String(validate=Length(max=EMAIL_ADDRESS_NOTE_MAX_LENGTH))
created_time = DateTime(dump_only=True)
bio_markdown = Markdown(max_length=BIO_MAX_LENGTH, allow_none=True)
words_per_minute = Integer(validate=Range(min=0, max=9999))
@post_dump
def anonymize_username(self, data: dict, many: bool) -> dict:

4
tildes/tildes/templates/settings.jinja2

@ -260,6 +260,10 @@
<a href="/settings/bio">Edit your user bio</a>
<div class="text-small text-secondary">Tell others about yourself with a short bio on your user page</div>
</li>
<li>
<a href="/settings/wpm">Edit your reading speed</a>
<div class="text-small text-secondary">Customize your reading speed for personalized reading time estimates</div>
</li>
<li>
<a href="/settings/account_recovery">Set up account recovery</a>
<div class="text-small text-secondary">To be able to regain access in case of lost password, compromise, etc.</div>

32
tildes/tildes/templates/settings_wpm.jinja2

@ -0,0 +1,32 @@
{# Copyright (c) 2023 Tildes contributors <code@tildes.net> #}
{# SPDX-License-Identifier: AGPL-3.0-or-later #}
{% extends 'base_settings.jinja2' %}
{% block title %}Edit your reading speed{% endblock %}
{% block main_heading %}Edit your reading speed{% endblock %}
{% block settings %}
<p>Enter your estimated reading speed in words per minute (WPM). This will be used to calculate estimated reading times for posts. To disable reading time from displaying on Link topics, enter a value of 0.</p>
<div class="divider"></div>
<form
method="post"
name="wpm-change"
autocomplete="off"
data-ic-patch-to="{{ request.route_url('ic_user', username=request.user.username) }}"
data-ic-trigger-name="user-wpm"
data-js-confirm-leave-page-unsaved
>
<div class="form-group">
<label class="form-label" for="wordsPerMinute">Words Per Minute (WPM)</label>
<input type="number" class="form-input" id="wordsPerMinute" name="words_per_minute" value="{{ request.user.words_per_minute }}" min="0" max="9999" required>
</div>
<div class="form-buttons">
<button type="submit" class="btn btn-primary">Save WPM</button>
</div>
</form>
{% endblock %}

43
tildes/tildes/templates/topic.jinja2

@ -93,7 +93,7 @@
{% endif %}
{% if content_metadata %}
{{ topic_content_metadata(content_metadata) }}
{{ topic_content_metadata(content_metadata, words_per_minute) }}
{% endif %}
{% endif %}
{% endif %}
@ -364,15 +364,38 @@
</dl>
{% endblock %}
{% macro topic_content_metadata(content_metadata) %}
{% macro topic_content_metadata(content_metadata, words_per_minute) %}
<section class="topic-full-content-metadata">
<h2>Link information</h2>
<p class="text-small text-secondary">This data is scraped automatically and may be incorrect.</p>
<dl>
{% for field, value in content_metadata.items() %}
<dt>{{ field }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
<h2>Link information</h2>
<p class="text-small text-secondary">This data is scraped automatically and may be incorrect.</p>
<dl>
{% for field, value in content_metadata.items() %}
<dt>{{ field }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
{% if words_per_minute > 0 %}
<dt>Reading time</dt>
<dd>
{% set word_count = content_metadata['Word count'].split()[0]|int %}
{% set reading_time = (word_count / words_per_minute)|round(0, 'ceil')|int %}
{% if reading_time >= 60 %}
{% set hours = (reading_time // 60)|int %}
{% set minutes = (reading_time % 60)|int %}
~{{ hours }} hour{{ 's' if hours > 1 }} and {{ minutes }} minute{{ 's' if minutes > 1 }}
{% else %}
{% if reading_time == 1 %}
~1 minute
{% else %}
~{{ reading_time }} minutes
{% endif %}
{% endif %}
</dd>
{% endif %}
</dl>
</section>
{% endmacro %}

4
tildes/tildes/views/api/web/exceptions.py

@ -75,8 +75,8 @@ def error_to_text_response(request: Request) -> Response:
elif isinstance(request.exception, HTTPBadRequest):
if response.title == "Bad CSRF Token":
response.text = "Page expired, reload and try again"
else:
response.text = "Unknown error"
# else:
# response.text = "Unknown error"
return response

21
tildes/tildes/views/api/web/user.py

@ -9,6 +9,7 @@ from typing import Optional
from marshmallow import ValidationError
from marshmallow.fields import String
from marshmallow.fields import Int
from pyramid.httpexceptions import (
HTTPForbidden,
HTTPUnauthorized,
@ -275,15 +276,15 @@ def patch_change_collapse_old_comments(request: Request) -> Response:
@ic_view_config(
route_name="user",
request_method="PATCH",
request_param="ic-trigger-name=account-default-theme",
request_param="ic-trigger-name=user-bio",
permission="change_settings",
)
def patch_change_account_default_theme(request: Request) -> Response:
"""Change the user's "theme account default" setting."""
@use_kwargs({"markdown": String()}, location="form")
def patch_change_user_bio(request: Request, markdown: str) -> dict:
"""Update a user's bio."""
user = request.context
new_theme = request.params.get("theme")
user.theme_default = new_theme
user.bio_markdown = markdown
return IC_NOOP
@ -291,15 +292,15 @@ def patch_change_account_default_theme(request: Request) -> Response:
@ic_view_config(
route_name="user",
request_method="PATCH",
request_param="ic-trigger-name=user-bio",
request_param="ic-trigger-name=wpm-change",
permission="change_settings",
)
@use_kwargs({"markdown": String()}, location="form")
def patch_change_user_bio(request: Request, markdown: str) -> dict:
"""Update a user's bio."""
@use_kwargs({"words_per_minute": Int()}, location="form")
def patch_change_wpm(request: Request, words_per_minute: int) -> dict:
"""Change the user's words per minute (WPM) setting."""
user = request.context
user.bio_markdown = markdown
user.words_per_minute = words_per_minute
return IC_NOOP

6
tildes/tildes/views/settings.py

@ -257,3 +257,9 @@ def get_settings_theme_previews(request: Request) -> dict:
"fake_comment_tree": fake_tree,
"last_visit": fake_last_visit_time,
}
@view_config(route_name="settings_wpm", renderer="settings_wpm.jinja2")
def get_settings_wpm(request: Request) -> dict:
"""Render the WPM settings page."""
return {}

6
tildes/tildes/views/topic.py

@ -464,7 +464,7 @@ def get_topic(request: Request, comment_order: CommentTreeSortOption) -> dict:
)
tree.collapse_from_labels()
user_wpm = 0 # Initialize user_wpm with a default value of 0
if request.user:
request.db_session.add(TopicVisit(request.user, topic))
@ -474,6 +474,9 @@ def get_topic(request: Request, comment_order: CommentTreeSortOption) -> dict:
tree.uncollapse_new_comments(topic.last_visit_time)
tree.finalize_collapsing_maximized()
# Get user's wpm setting or use a default if not set
user_wpm = request.user.words_per_minute or 0
return {
"topic": topic,
"content_metadata": content_metadata,
@ -482,6 +485,7 @@ def get_topic(request: Request, comment_order: CommentTreeSortOption) -> dict:
"comment_order": comment_order,
"comment_order_options": CommentTreeSortOption,
"comment_label_options": CommentLabelOption,
"words_per_minute": user_wpm,
}

Loading…
Cancel
Save