Browse Source

Merge branch 'upgrade-marshmallow-4.0' into 'develop-1.101'

Draft: Upgrade Marshmallow to 4.0

See merge request tildes/tildes!171
merge-requests/171/merge
talklittle 4 weeks ago
parent
commit
af863745a7
  1. 4
      tildes/requirements-dev.txt
  2. 4
      tildes/requirements.in
  3. 4
      tildes/requirements.txt
  4. 6
      tildes/tildes/json.py
  5. 4
      tildes/tildes/resources/group.py
  6. 2
      tildes/tildes/schemas/comment.py
  7. 30
      tildes/tildes/schemas/context.py
  8. 25
      tildes/tildes/schemas/fields.py
  9. 17
      tildes/tildes/schemas/group.py
  10. 27
      tildes/tildes/schemas/listing.py
  11. 24
      tildes/tildes/schemas/topic.py
  12. 29
      tildes/tildes/schemas/user.py
  13. 2
      tildes/tildes/views/api/web/comment.py
  14. 2
      tildes/tildes/views/api/web/group.py
  15. 3
      tildes/tildes/views/api/web/topic.py
  16. 8
      tildes/tildes/views/api/web/user.py
  17. 2
      tildes/tildes/views/bookmarks.py
  18. 28
      tildes/tildes/views/decorators.py
  19. 13
      tildes/tildes/views/login.py
  20. 2
      tildes/tildes/views/message.py
  21. 8
      tildes/tildes/views/register.py
  22. 6
      tildes/tildes/views/settings.py
  23. 8
      tildes/tildes/views/topic.py
  24. 8
      tildes/tildes/views/user.py
  25. 2
      tildes/tildes/views/votes.py

4
tildes/requirements-dev.txt

@ -43,7 +43,7 @@ lazy-object-proxy==1.11.0
lupa==2.5
mako==1.3.10
markupsafe==3.0.2
marshmallow==3.25.1
marshmallow==4.0.1
matplotlib-inline==0.1.7
mccabe==0.7.0
more-itertools==10.7.0
@ -135,7 +135,7 @@ urllib3==2.5.0
venusian==3.1.1
waitress==3.0.2
wcwidth==0.2.13
webargs==8.0.0
webargs==8.7.0
webassets==2.0
webencodings==0.5.1
webob==1.8.9

4
tildes/requirements.in

@ -10,7 +10,7 @@ html5lib
invoke
ipython
lupa
marshmallow==3.25.1 # TODO: Upgrade Marshmallow https://marshmallow.readthedocs.io/en/latest/upgrading.html
marshmallow
Pillow
pip-tools
prometheus-client
@ -37,6 +37,6 @@ SQLAlchemy-Utils
stripe==2.6.0 # TODO: Figure out if we can update this
titlecase
unicodedata2
webargs==8.0.0 # TODO: Updating webargs causes an issue with parsing URLs, figure out if we can fix this
webargs
wrapt
zope.sqlalchemy==1.5 # TODO: Figure out if we can update this

4
tildes/requirements.txt

@ -31,7 +31,7 @@ lazy-object-proxy==1.11.0
lupa==2.5
mako==1.3.10
markupsafe==3.0.2
marshmallow==3.25.1
marshmallow==4.0.1
matplotlib-inline==0.1.7
more-itertools==10.7.0
openapi-core==0.19.5
@ -89,7 +89,7 @@ unicodedata2==16.0.0
urllib3==2.5.0
venusian==3.1.1
wcwidth==0.2.13
webargs==8.0.0
webargs==8.7.0
webassets==2.0
webencodings==0.5.1
webob==1.8.9

6
tildes/tildes/json.py

@ -11,6 +11,7 @@ from tildes.models import DatabaseModel
from tildes.models.group import Group
from tildes.models.topic import Topic
from tildes.models.user import User
from tildes.schemas.context import TildesSchemaContext, TildesContext
def serialize_model(model_item: DatabaseModel, request: Request) -> dict:
@ -25,11 +26,12 @@ def serialize_model(model_item: DatabaseModel, request: Request) -> dict:
def serialize_topic(topic: Topic, request: Request) -> dict:
"""Return serializable data for a Topic."""
context = {}
context: TildesContext = {}
if not request.has_permission("view_author", topic):
context["hide_username"] = True
return topic.schema_class(context=context).dump(topic)
with TildesSchemaContext(context):
return topic.schema_class().dump(topic)
def includeme(config: Configurator) -> None:

4
tildes/tildes/resources/group.py

@ -10,12 +10,14 @@ from sqlalchemy_utils import Ltree
from tildes.models.group import Group, GroupWikiPage
from tildes.resources import get_resource
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.group import GroupSchema
from tildes.views.decorators import use_kwargs
@use_kwargs(
GroupSchema(only=("path",), context={"fix_path_capitalization": True}),
GroupSchema(only=("path",)),
context=TildesSchemaContext(TildesContext(fix_path_capitalization=True)),
location="matchdict",
)
def group_by_path(request: Request, path: str) -> Group:

2
tildes/tildes/schemas/comment.py

@ -21,4 +21,4 @@ class CommentLabelSchema(Schema):
"""Marshmallow schema for comment labels."""
name = Enum(CommentLabelOption)
reason = SimpleString(max_length=1000, missing=None)
reason = SimpleString(max_length=1000, load_default=None)

30
tildes/tildes/schemas/context.py

@ -0,0 +1,30 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Context variables that can be used with Marshmallow schemas."""
import typing
from marshmallow.experimental.context import Context
class TildesContext(typing.TypedDict, total=False):
"""Context for Tildes Marshmallow schemas.
For convenience, we use one unified class instead of one per schema,
so it can be passed down through different schemas in a subgraph.
For example, if a Topic contains a reference to a User,
one instance of TildesContext can configure both the Topic and User.
"""
# Applies to UserSchema
hide_username: bool
# Applies to UserSchema
check_breached_passwords: bool
# Applies to UserSchema
username_trim_whitespace: bool
# Applies to GroupSchema
fix_path_capitalization: bool
TildesSchemaContext = Context[TildesContext]

25
tildes/tildes/schemas/fields.py

@ -22,7 +22,7 @@ from tildes.lib.string import simplify_string
DataType = Optional[Mapping[str, Any]]
class Enum(Field):
class Enum(Field[enum.Enum]):
"""Field for a native Python Enum (or subclasses)."""
def __init__(
@ -34,9 +34,12 @@ class Enum(Field):
self._enum_class = enum_class
def _serialize(
self, value: enum.Enum, attr: str | None, obj: object, **kwargs: Any
) -> str:
self, value: enum.Enum | None, attr: str | None, obj: object, **kwargs: Any
) -> str | None:
"""Serialize the enum value - lowercase version of its name."""
if value is None:
return None
return value.name.lower()
def _deserialize(
@ -64,7 +67,7 @@ class ID36(String):
super().__init__(validate=Regexp(ID36_REGEX), **kwargs)
class ShortTimePeriod(Field):
class ShortTimePeriod(Field[Optional[SimpleHoursPeriod]]):
"""Field for short time period strings like "4h" and "2d".
Also supports the string "all" which will be converted to None.
@ -100,7 +103,7 @@ class ShortTimePeriod(Field):
return value.as_short_form()
class Markdown(Field):
class Markdown(Field[str]):
"""Field for markdown strings (comments, text topic, messages, etc.)."""
DEFAULT_MAX_LENGTH = 50000
@ -132,13 +135,13 @@ class Markdown(Field):
return value
def _serialize(
self, value: str, attr: str | None, obj: object, **kwargs: Any
) -> str:
self, value: str | None, attr: str | None, obj: object, **kwargs: Any
) -> str | None:
"""Serialize the value (no-op in this case)."""
return value
class SimpleString(Field):
class SimpleString(Field[str]):
"""Field for "simple" strings, suitable for uses like subject, title, etc.
These strings should generally not contain any special formatting (such as
@ -169,13 +172,13 @@ class SimpleString(Field):
return simplify_string(value)
def _serialize(
self, value: str, attr: str | None, obj: object, **kwargs: Any
) -> str:
self, value: str | None, attr: str | None, obj: object, **kwargs: Any
) -> str | None:
"""Serialize the value (no-op in this case)."""
return value
class Ltree(Field):
class Ltree(Field[sqlalchemy_utils.Ltree]):
"""Field for postgresql ltree type."""
# note that this regex only checks whether all of the chars are individually valid,

17
tildes/tildes/schemas/group.py

@ -10,7 +10,9 @@ import sqlalchemy_utils
from marshmallow import pre_load, Schema, validates
from marshmallow.exceptions import ValidationError
from marshmallow.fields import DateTime
from marshmallow.types import UnknownOption
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.fields import Ltree, Markdown, SimpleString
@ -41,10 +43,14 @@ class GroupSchema(Schema):
sidebar_markdown = Markdown(allow_none=True)
@pre_load
def prepare_path(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_path(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the path value before it's validated."""
# pylint: disable=unused-argument
if not self.context.get("fix_path_capitalization"):
if not TildesSchemaContext.get(default=TildesContext()).get(
"fix_path_capitalization"
):
return data
if "path" not in data or not isinstance(data["path"], str):
@ -57,8 +63,9 @@ class GroupSchema(Schema):
return new_data
@validates("path")
def validate_path(self, value: sqlalchemy_utils.Ltree) -> None:
def validate_path(self, value: sqlalchemy_utils.Ltree, data_key: str) -> None:
"""Validate the path field, raising an error if an issue exists."""
# pylint: disable=unused-argument
# check each element for length and against validity regex
path_elements = value.path.split(".")
for element in path_elements:
@ -69,7 +76,9 @@ class GroupSchema(Schema):
raise ValidationError("Path element %s is invalid" % element)
@pre_load
def prepare_sidebar_markdown(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_sidebar_markdown(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the sidebar_markdown value before it's validated."""
# pylint: disable=unused-argument
if "sidebar_markdown" not in data:

27
tildes/tildes/schemas/listing.py

@ -7,6 +7,7 @@ from typing import Any
from marshmallow import pre_load, Schema, validates_schema, ValidationError
from marshmallow.fields import Boolean, Integer
from marshmallow.types import UnknownOption
from marshmallow.validate import Range
from tildes.enums import TopicSortOption
@ -16,12 +17,14 @@ from tildes.schemas.fields import Enum, ID36, Ltree, PostType, ShortTimePeriod
class PaginatedListingSchema(Schema):
"""Marshmallow schema to validate arguments for a paginated listing page."""
after = ID36(missing=None)
before = ID36(missing=None)
per_page = Integer(validate=Range(min=1, max=100), missing=50)
after = ID36(load_default=None)
before = ID36(load_default=None)
per_page = Integer(validate=Range(min=1, max=100), load_default=50)
@validates_schema
def either_after_or_before(self, data: dict, many: bool, partial: Any) -> None:
def either_after_or_before(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> None:
"""Fail validation if both after and before were specified."""
# pylint: disable=unused-argument
if data.get("after") and data.get("before"):
@ -32,15 +35,15 @@ class TopicListingSchema(PaginatedListingSchema):
"""Marshmallow schema to validate arguments for a topic listing page."""
period = ShortTimePeriod(allow_none=True)
order = Enum(TopicSortOption, missing=None)
tag = Ltree(missing=None)
unfiltered = Boolean(missing=False)
all_subgroups = Boolean(missing=False)
rank_start = Integer(data_key="n", validate=Range(min=1), missing=None)
order = Enum(TopicSortOption, load_default=None)
tag = Ltree(load_default=None)
unfiltered = Boolean(load_default=False)
all_subgroups = Boolean(load_default=False)
rank_start = Integer(data_key="n", validate=Range(min=1), load_default=None)
@pre_load
def reset_rank_start_on_first_page(
self, data: dict, many: bool, partial: Any
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Reset rank_start to 1 if this is a first page (no before/after)."""
# pylint: disable=unused-argument
@ -62,11 +65,11 @@ class MixedListingSchema(PaginatedListingSchema):
of just one or the other.
"""
anchor_type = PostType(missing=None)
anchor_type = PostType(load_default=None)
@pre_load
def set_anchor_type_from_before_or_after(
self, data: dict, many: bool, partial: Any
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Set the anchor_type if before or after has a special value indicating type.

24
tildes/tildes/schemas/topic.py

@ -9,6 +9,7 @@ from urllib.parse import urlparse
from marshmallow import pre_load, Schema, validates, validates_schema, ValidationError
from marshmallow.fields import DateTime, List, Nested, String, URL
from marshmallow.types import UnknownOption
from tildes.lib.url_transform import apply_url_transformations
from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString
@ -36,7 +37,9 @@ class TopicSchema(Schema):
group = Nested(GroupSchema, dump_only=True)
@pre_load
def prepare_title(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_title(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the title before it's validated."""
# pylint: disable=unused-argument
if "title" not in data:
@ -56,7 +59,9 @@ class TopicSchema(Schema):
return new_data
@pre_load
def prepare_tags(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_tags(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the tags before they're validated."""
# pylint: disable=unused-argument
if "tags" not in data:
@ -98,7 +103,7 @@ class TopicSchema(Schema):
return new_data
@validates("tags")
def validate_tags(self, value: list[str]) -> None:
def validate_tags(self, value: list[str], data_key: str) -> None:
"""Validate the tags field, raising an error if an issue exists.
Note that tags are validated by ensuring that each tag would be a valid group
@ -107,6 +112,7 @@ class TopicSchema(Schema):
between groups and tags. For example, a popular tag in a group could be
converted into a sub-group easily.
"""
# pylint: disable=unused-argument
group_schema = GroupSchema(partial=True)
for tag in value:
try:
@ -115,7 +121,9 @@ class TopicSchema(Schema):
raise ValidationError("Tag %s is invalid" % tag) from exc
@pre_load
def prepare_markdown(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_markdown(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the markdown value before it's validated."""
# pylint: disable=unused-argument
if "markdown" not in data:
@ -130,7 +138,9 @@ class TopicSchema(Schema):
return new_data
@pre_load
def prepare_link(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_link(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the link value before it's validated."""
# pylint: disable=unused-argument
if "link" not in data:
@ -157,7 +167,9 @@ class TopicSchema(Schema):
return new_data
@validates_schema
def link_or_markdown(self, data: dict, many: bool, partial: Any) -> None:
def link_or_markdown(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> None:
"""Fail validation unless at least one of link or markdown were set."""
# pylint: disable=unused-argument
if "link" not in data and "markdown" not in data:

29
tildes/tildes/schemas/user.py

@ -9,9 +9,11 @@ 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.types import UnknownOption
from marshmallow.validate import Length, Regexp
from tildes.lib.password import is_breached_password
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.fields import Markdown
@ -63,7 +65,7 @@ class UserSchema(Schema):
def anonymize_username(self, data: dict, many: bool) -> dict:
"""Hide the username if the dumping context specifies to do so."""
# pylint: disable=unused-argument
if not self.context.get("hide_username"):
if not TildesSchemaContext.get(default=TildesContext()).get("hide_username"):
return data
if "username" not in data:
@ -77,7 +79,7 @@ class UserSchema(Schema):
@validates_schema
def username_pass_not_substrings(
self, data: dict, many: bool, partial: Any
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> None:
"""Ensure the username isn't in the password and vice versa."""
# pylint: disable=unused-argument
@ -96,12 +98,15 @@ class UserSchema(Schema):
raise ValidationError("Username cannot contain password")
@validates("password")
def password_not_breached(self, value: str) -> None:
def password_not_breached(self, value: str, data_key: str) -> None:
"""Validate that the password is not in the breached-passwords list.
Requires check_breached_passwords be True in the schema's context.
"""
if not self.context.get("check_breached_passwords"):
# pylint: disable=unused-argument
if not TildesSchemaContext.get(default=TildesContext()).get(
"check_breached_passwords"
):
return
if is_breached_password(value):
@ -111,13 +116,17 @@ class UserSchema(Schema):
)
@pre_load
def username_trim_whitespace(self, data: dict, many: bool, partial: Any) -> dict:
def username_trim_whitespace(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Trim leading/trailing whitespace around the username.
Requires username_trim_whitespace be True in the schema's context.
"""
# pylint: disable=unused-argument
if not self.context.get("username_trim_whitespace"):
if not TildesSchemaContext.get(default=TildesContext()).get(
"username_trim_whitespace"
):
return data
if "username" not in data:
@ -130,7 +139,9 @@ class UserSchema(Schema):
return new_data
@pre_load
def prepare_email_address(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_email_address(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the email address value before it's validated."""
# pylint: disable=unused-argument
if "email_address" not in data:
@ -148,7 +159,9 @@ class UserSchema(Schema):
return new_data
@pre_load
def prepare_bio_markdown(self, data: dict, many: bool, partial: Any) -> dict:
def prepare_bio_markdown(
self, data: dict, many: bool, partial: Any, unknown: UnknownOption
) -> dict:
"""Prepare the bio_markdown value before it's validated."""
# pylint: disable=unused-argument
if "bio_markdown" not in data:

2
tildes/tildes/views/api/web/comment.py

@ -335,7 +335,7 @@ def delete_label_comment(request: Request, name: CommentLabelOption) -> Response
@ic_view_config(
route_name="comment_mark_read", request_method="PUT", permission="mark_read"
)
@use_kwargs({"mark_all_previous": Boolean(missing=False)}, location="query")
@use_kwargs({"mark_all_previous": Boolean(load_default=False)}, location="query")
def put_mark_comments_read(request: Request, mark_all_previous: bool) -> Response:
"""Mark comment(s) read, clearing notifications.

2
tildes/tildes/views/api/web/group.py

@ -86,7 +86,7 @@ def delete_subscribe_group(request: Request) -> dict:
@use_kwargs(
{
"order": Enum(TopicSortOption),
"period": ShortTimePeriod(allow_none=True, missing=None),
"period": ShortTimePeriod(allow_none=True, load_default=None),
},
location="form",
)

3
tildes/tildes/views/api/web/topic.py

@ -158,7 +158,8 @@ def get_topic_tags(request: Request) -> dict:
permission="tag",
)
@use_kwargs(
{"tags": String(missing=""), "conflict_check": String(missing="")}, location="form"
{"tags": String(load_default=""), "conflict_check": String(load_default="")},
location="form",
)
def put_tag_topic(request: Request, tags: str, conflict_check: str) -> dict:
"""Apply tags to a topic with Intercooler."""

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

@ -23,6 +23,7 @@ from tildes.lib.datetime import SimpleHoursPeriod
from tildes.lib.string import separate_string
from tildes.models.log import Log
from tildes.models.user import User, UserInviteCode
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.fields import Enum, ShortTimePeriod
from tildes.schemas.topic import TopicSchema
from tildes.schemas.user import UserSchema
@ -54,12 +55,13 @@ def patch_change_password(
user = request.context
# enable checking the new password against the breached-passwords list
user.schema.context["check_breached_passwords"] = True
context: TildesContext = {"check_breached_passwords": True}
if new_password != new_password_confirm:
raise HTTPUnprocessableEntity("New password and confirmation do not match.")
user.change_password(old_password, new_password)
with TildesSchemaContext(context):
user.change_password(old_password, new_password)
return Response("Your password has been updated")
@ -357,7 +359,7 @@ def get_invite_code(request: Request) -> dict:
@use_kwargs(
{
"order": Enum(TopicSortOption),
"period": ShortTimePeriod(allow_none=True, missing=None),
"period": ShortTimePeriod(allow_none=True, load_default=None),
},
location="form",
)

2
tildes/tildes/views/bookmarks.py

@ -15,7 +15,7 @@ from tildes.views.decorators import use_kwargs
@view_config(route_name="bookmarks", renderer="bookmarks.jinja2")
@use_kwargs(PaginatedListingSchema())
@use_kwargs({"post_type": PostType(data_key="type", missing="topic")})
@use_kwargs({"post_type": PostType(data_key="type", load_default="topic")})
def get_bookmarks(
request: Request,
after: Optional[str],

28
tildes/tildes/views/decorators.py

@ -4,9 +4,10 @@
"""Contains decorators for view functions."""
from collections.abc import Callable
from typing import Any, Union
from typing import Any
from marshmallow import EXCLUDE
from marshmallow.experimental.context import Context
from marshmallow.fields import Field
from marshmallow.schema import Schema
from pyramid.httpexceptions import HTTPFound
@ -14,9 +15,14 @@ from pyramid.request import Request
from pyramid.view import view_config
from webargs import pyramidparser
from tildes.schemas.context import TildesSchemaContext, TildesContext
def use_kwargs(
argmap: Union[Schema, dict[str, Field]], location: str = "query", **kwargs: Any
argmap: Schema | dict[str, Field],
location: str = "query",
context: Context[Any] | None = None,
**kwargs: Any
) -> Callable:
"""Wrap the webargs @use_kwargs decorator with preferred default modifications.
@ -28,15 +34,21 @@ def use_kwargs(
it just ignores them, instead of erroring when there's unexpected data (as there
almost always is, especially because of Intercooler).
"""
# convert a dict argmap to a Schema (the same way webargs would on its own)
if isinstance(argmap, dict):
argmap = Schema.from_dict(argmap)()
if context is None:
context = TildesSchemaContext(TildesContext())
with context:
# convert a dict argmap to a Schema (the same way webargs would on its own)
if isinstance(argmap, dict):
argmap = Schema.from_dict(argmap)()
assert isinstance(argmap, Schema) # tell mypy the type is more restricted now
assert isinstance(argmap, Schema) # tell mypy the type is more restricted now
argmap.unknown = EXCLUDE
argmap.unknown = EXCLUDE
return pyramidparser.use_kwargs(argmap, location=location, **kwargs)
return pyramidparser.use_kwargs(
argmap, location=location, unknown=None, **kwargs
)
def ic_view_config(**kwargs: Any) -> Callable:

13
tildes/tildes/views/login.py

@ -19,6 +19,7 @@ from tildes.enums import LogEventType
from tildes.metrics import incr_counter
from tildes.models.log import Log
from tildes.models.user import User
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.user import UserSchema
from tildes.views.decorators import not_logged_in, rate_limit_view, use_kwargs
@ -26,7 +27,7 @@ from tildes.views.decorators import not_logged_in, rate_limit_view, use_kwargs
@view_config(
route_name="login", renderer="login.jinja2", permission=NO_PERMISSION_REQUIRED
)
@use_kwargs({"from_url": String(missing="")})
@use_kwargs({"from_url": String(load_default="")})
@not_logged_in
def get_login(request: Request, from_url: str) -> dict:
"""Display the login form."""
@ -60,12 +61,11 @@ def finish_login(request: Request, user: User, redirect_url: str) -> HTTPFound:
route_name="login", request_method="POST", permission=NO_PERMISSION_REQUIRED
)
@use_kwargs(
UserSchema(
only=("username", "password"), context={"username_trim_whitespace": True}
),
UserSchema(only=("username", "password")),
context=TildesSchemaContext(TildesContext(username_trim_whitespace=True)),
location="form",
)
@use_kwargs({"from_url": String(missing="")}, location="form")
@use_kwargs({"from_url": String(load_default="")}, location="form")
@not_logged_in
@rate_limit_view("login")
def post_login(
@ -148,7 +148,8 @@ def post_login(
@not_logged_in
@rate_limit_view("login_two_factor")
@use_kwargs(
{"code": String(missing=""), "from_url": String(missing="")}, location="form"
{"code": String(load_default=""), "from_url": String(load_default="")},
location="form",
)
def post_login_two_factor(request: Request, code: str, from_url: str) -> NoReturn:
"""Process a log in request with 2FA."""

2
tildes/tildes/views/message.py

@ -18,7 +18,7 @@ from tildes.views.decorators import use_kwargs
@view_config(
route_name="new_message", renderer="new_message.jinja2", permission="message"
)
@use_kwargs({"subject": String(missing=""), "message": String(missing="")})
@use_kwargs({"subject": String(load_default=""), "message": String(load_default="")})
def get_new_message_form(request: Request, subject: str, message: str) -> dict:
"""Form for entering a new private message to send."""
return {

8
tildes/tildes/views/register.py

@ -15,6 +15,7 @@ from tildes.metrics import incr_counter
from tildes.models.group import Group, GroupSubscription
from tildes.models.log import Log
from tildes.models.user import User, UserInviteCode
from tildes.schemas.context import TildesSchemaContext, TildesContext
from tildes.schemas.user import UserSchema
from tildes.views.decorators import not_logged_in, rate_limit_view, use_kwargs
@ -22,7 +23,7 @@ from tildes.views.decorators import not_logged_in, rate_limit_view, use_kwargs
@view_config(
route_name="register", renderer="register.jinja2", permission=NO_PERMISSION_REQUIRED
)
@use_kwargs({"code": String(missing="")})
@use_kwargs({"code": String(load_default="")})
@not_logged_in
def get_register(request: Request, code: str) -> dict:
"""Display the registration form."""
@ -34,9 +35,8 @@ def get_register(request: Request, code: str) -> dict:
route_name="register", request_method="POST", permission=NO_PERMISSION_REQUIRED
)
@use_kwargs(
UserSchema(
only=("username", "password"), context={"check_breached_passwords": True}
),
UserSchema(only=("username", "password")),
context=TildesSchemaContext(TildesContext(check_breached_passwords=True)),
location="form",
)
@use_kwargs(

6
tildes/tildes/views/settings.py

@ -22,6 +22,7 @@ from tildes.models.comment import Comment, CommentLabel, CommentTree
from tildes.models.group import Group
from tildes.models.topic import Topic
from tildes.models.user import User
from tildes.schemas.context import TildesContext, TildesSchemaContext
from tildes.schemas.user import (
BIO_MAX_LENGTH,
EMAIL_ADDRESS_NOTE_MAX_LENGTH,
@ -151,12 +152,13 @@ def post_settings_password_change(
) -> Response:
"""Change the logged-in user's password."""
# enable checking the new password against the breached-passwords list
request.user.schema.context["check_breached_passwords"] = True
context: TildesContext = {"check_breached_passwords": True}
if new_password != new_password_confirm:
raise HTTPUnprocessableEntity("New password and confirmation do not match.")
request.user.change_password(old_password, new_password)
with TildesSchemaContext(context):
request.user.change_password(old_password, new_password)
return Response("Your password has been updated")

8
tildes/tildes/views/topic.py

@ -48,7 +48,7 @@ DefaultSettings = namedtuple("DefaultSettings", ["order", "period"])
@view_config(route_name="group_topics", request_method="POST", permission="topic.post")
@use_kwargs(TopicSchema(only=("title", "markdown", "link")), location="form")
@use_kwargs(
{"tags": String(missing=""), "confirm_repost": Boolean(missing=False)},
{"tags": String(load_default=""), "confirm_repost": Boolean(load_default=False)},
location="form",
)
def post_group_topics(
@ -345,7 +345,7 @@ def get_group_topics( # noqa
@view_config(route_name="search", renderer="search.jinja2")
@view_config(route_name="group_search", renderer="search.jinja2")
@use_kwargs(TopicListingSchema(only=("after", "before", "order", "per_page", "period")))
@use_kwargs({"search": String(data_key="q", missing="")})
@use_kwargs({"search": String(data_key="q", load_default="")})
def get_search(
request: Request,
order: Optional[TopicSortOption],
@ -414,7 +414,7 @@ def get_search(
@view_config(
route_name="new_topic", renderer="new_topic.jinja2", permission="topic.post"
)
@use_kwargs({"title": String(missing=""), "link": String(missing="")})
@use_kwargs({"title": String(load_default=""), "link": String(load_default="")})
def get_new_topic_form(request: Request, title: str, link: str) -> dict:
"""Form for entering a new topic to post."""
group = request.context
@ -424,7 +424,7 @@ def get_new_topic_form(request: Request, title: str, link: str) -> dict:
@view_config(route_name="topic", renderer="topic.jinja2")
@view_config(route_name="topic_no_title", renderer="topic.jinja2")
@use_kwargs({"comment_order": Enum(CommentTreeSortOption, missing=None)})
@use_kwargs({"comment_order": Enum(CommentTreeSortOption, load_default=None)})
def get_topic(request: Request, comment_order: CommentTreeSortOption) -> dict:
"""View a single topic."""
topic = request.context

8
tildes/tildes/views/user.py

@ -25,8 +25,8 @@ from tildes.views.decorators import use_kwargs
@use_kwargs(MixedListingSchema())
@use_kwargs(
{
"post_type": PostType(data_key="type", missing=None),
"order_name": String(data_key="order", missing="new"),
"post_type": PostType(data_key="type", load_default=None),
"order_name": String(data_key="order", load_default="new"),
}
)
def get_user(
@ -94,8 +94,8 @@ def get_user(
@use_kwargs(
{
"post_type": PostType(data_key="type", required=True),
"order_name": String(data_key="order", missing="new"),
"search": String(data_key="q", missing=""),
"order_name": String(data_key="order", load_default="new"),
"search": String(data_key="q", load_default=""),
}
)
def get_user_search(

2
tildes/tildes/views/votes.py

@ -15,7 +15,7 @@ from tildes.views.decorators import use_kwargs
@view_config(route_name="votes", renderer="votes.jinja2")
@use_kwargs(PaginatedListingSchema())
@use_kwargs({"post_type": PostType(data_key="type", missing="topic")})
@use_kwargs({"post_type": PostType(data_key="type", load_default="topic")})
def get_voted_posts(
request: Request,
after: Optional[str],

Loading…
Cancel
Save