Browse Source

Upgrade Marshmallow to 4.0

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

4
tildes/requirements-dev.txt

@ -37,7 +37,7 @@ jinja2==3.1.6
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
mypy==1.17.1
@ -119,7 +119,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
@ -36,6 +36,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

@ -25,7 +25,7 @@ jinja2==3.1.6
lupa==2.5
mako==1.3.10
markupsafe==3.0.2
marshmallow==3.25.1
marshmallow==4.0.1
matplotlib-inline==0.1.7
packaging==25.0
parso==0.8.4
@ -73,7 +73,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

5
tildes/tests/test_markdown_field.py

@ -1,13 +1,14 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
from marshmallow import Schema, ValidationError
from marshmallow import ValidationError
from pytest import raises
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import Markdown
class MarkdownFieldTestSchema(Schema):
class MarkdownFieldTestSchema(BaseTildesSchema):
"""Simple schema class with a standard Markdown field."""
markdown = Markdown()

5
tildes/tests/test_simplestring_field.py

@ -1,13 +1,14 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
from marshmallow import Schema, ValidationError
from marshmallow import ValidationError
from pytest import raises
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import SimpleString
class SimpleStringTestSchema(Schema):
class SimpleStringTestSchema(BaseTildesSchema):
"""Simple schema class with a standard SimpleString field."""
subject = SimpleString()

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, TildesSchemaContextDict
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: TildesSchemaContextDict = {}
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:

37
tildes/tildes/schemas/base.py

@ -0,0 +1,37 @@
# Copyright (c) 2018 Tildes contributors <code@tildes.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Base Marshmallow schema."""
from typing import Any, Optional
from marshmallow import Schema
from tildes.schemas.context import TildesSchemaContext, TildesSchemaContextDict
class BaseTildesSchema(Schema):
"""Base Marshmallow schema for Tildes schemas.
Adds common code like the context dict.
"""
context: TildesSchemaContextDict
def __init__(
self, context: Optional[TildesSchemaContextDict] = None, **kwargs: Any
):
"""Pass an optional context, and forward Schema arguments to superclass."""
super().__init__(**kwargs)
self.context = context if context else {}
def get_context_value(self, key: str) -> Any:
"""Get a value from the context dict.
Any active TildesSchemaContext, e.g. set using a "with" statement,
takes precedence. If there is no active TildesSchemaContext, then
it takes the value from the dict passed in __init__ instead.
"""
result = TildesSchemaContext.get(default=self.context).get(key)
if result:
return result
return self.context.get(key)

9
tildes/tildes/schemas/comment.py

@ -3,13 +3,12 @@
"""Validation/dumping schema for comments."""
from marshmallow import Schema
from tildes.enums import CommentLabelOption
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString
class CommentSchema(Schema):
class CommentSchema(BaseTildesSchema):
"""Marshmallow schema for comments."""
comment_id36 = ID36()
@ -17,8 +16,8 @@ class CommentSchema(Schema):
parent_comment_id36 = ID36()
class CommentLabelSchema(Schema):
class CommentLabelSchema(BaseTildesSchema):
"""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 TildesSchemaContextDict(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[TildesSchemaContextDict]

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,

19
tildes/tildes/schemas/group.py

@ -7,10 +7,12 @@ import re
from typing import Any
import sqlalchemy_utils
from marshmallow import pre_load, Schema, validates
from marshmallow import pre_load, validates
from marshmallow.exceptions import ValidationError
from marshmallow.fields import DateTime
from marshmallow.types import UnknownOption
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import Ltree, Markdown, SimpleString
@ -30,7 +32,7 @@ GROUP_PATH_ELEMENT_VALID_REGEX = re.compile(
SHORT_DESCRIPTION_MAX_LENGTH = 200
class GroupSchema(Schema):
class GroupSchema(BaseTildesSchema):
"""Marshmallow schema for groups."""
path = Ltree(required=True)
@ -41,10 +43,12 @@ 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 self.get_context_value("fix_path_capitalization"):
return data
if "path" not in data or not isinstance(data["path"], str):
@ -57,8 +61,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 +74,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:

5
tildes/tildes/schemas/group_wiki_page.py

@ -3,15 +3,14 @@
"""Validation/dumping schema for group wiki pages."""
from marshmallow import Schema
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import Markdown, SimpleString
PAGE_NAME_MAX_LENGTH = 40
class GroupWikiPageSchema(Schema):
class GroupWikiPageSchema(BaseTildesSchema):
"""Marshmallow schema for group wiki pages."""
page_name = SimpleString(max_length=PAGE_NAME_MAX_LENGTH)

32
tildes/tildes/schemas/listing.py

@ -5,23 +5,27 @@
from typing import Any
from marshmallow import pre_load, Schema, validates_schema, ValidationError
from marshmallow import pre_load, validates_schema, ValidationError
from marshmallow.fields import Boolean, Integer
from marshmallow.types import UnknownOption
from marshmallow.validate import Range
from tildes.enums import TopicSortOption
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import Enum, ID36, Ltree, PostType, ShortTimePeriod
class PaginatedListingSchema(Schema):
class PaginatedListingSchema(BaseTildesSchema):
"""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 +36,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 +66,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.

6
tildes/tildes/schemas/message.py

@ -3,16 +3,16 @@
"""Validation/dumping schemas for messages."""
from marshmallow import Schema
from marshmallow.fields import DateTime, String
from tildes.schemas.base import BaseTildesSchema
from tildes.schemas.fields import ID36, Markdown, SimpleString
SUBJECT_MAX_LENGTH = 200
class MessageConversationSchema(Schema):
class MessageConversationSchema(BaseTildesSchema):
"""Marshmallow schema for message conversations."""
conversation_id36 = ID36()
@ -22,7 +22,7 @@ class MessageConversationSchema(Schema):
created_time = DateTime(dump_only=True)
class MessageReplySchema(Schema):
class MessageReplySchema(BaseTildesSchema):
"""Marshmallow schema for message replies."""
reply_id36 = ID36()

29
tildes/tildes/schemas/topic.py

@ -7,10 +7,12 @@ import re
from typing import Any
from urllib.parse import urlparse
from marshmallow import pre_load, Schema, validates, validates_schema, ValidationError
from marshmallow import pre_load, 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.base import BaseTildesSchema
from tildes.schemas.fields import Enum, ID36, Markdown, SimpleString
from tildes.schemas.group import GroupSchema
from tildes.schemas.user import UserSchema
@ -20,7 +22,7 @@ TITLE_MAX_LENGTH = 200
TAG_SYNONYMS = {"spoiler": ["spoilers"]}
class TopicSchema(Schema):
class TopicSchema(BaseTildesSchema):
"""Marshmallow schema for topics."""
topic_id36 = ID36()
@ -36,7 +38,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 +60,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 +104,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 +113,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 +122,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 +139,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 +168,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

@ -6,12 +6,14 @@
import re
from typing import Any
from marshmallow import post_dump, pre_load, Schema, validates, validates_schema
from marshmallow import post_dump, pre_load, 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.base import BaseTildesSchema
from tildes.schemas.fields import Markdown
@ -41,7 +43,7 @@ EMAIL_ADDRESS_NOTE_MAX_LENGTH = 100
BIO_MAX_LENGTH = 2000
class UserSchema(Schema):
class UserSchema(BaseTildesSchema):
"""Marshmallow schema for users."""
username = String(
@ -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 self.get_context_value("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,13 @@ 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 self.get_context_value("check_breached_passwords"):
return
if is_breached_password(value):
@ -111,13 +114,15 @@ 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 self.get_context_value("username_trim_whitespace"):
return data
if "username" not in data:
@ -130,7 +135,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 +155,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, TildesSchemaContextDict
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: TildesSchemaContextDict = {"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],

6
tildes/tildes/views/decorators.py

@ -4,7 +4,7 @@
"""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.fields import Field
@ -16,7 +16,7 @@ from webargs import pyramidparser
def use_kwargs(
argmap: Union[Schema, dict[str, Field]], location: str = "query", **kwargs: Any
argmap: Schema | dict[str, Field], location: str = "query", **kwargs: Any
) -> Callable:
"""Wrap the webargs @use_kwargs decorator with preferred default modifications.
@ -36,7 +36,7 @@ def use_kwargs(
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:

7
tildes/tildes/views/login.py

@ -26,7 +26,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."""
@ -65,7 +65,7 @@ def finish_login(request: Request, user: User, redirect_url: str) -> HTTPFound:
),
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 {

2
tildes/tildes/views/register.py

@ -22,7 +22,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."""

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 TildesSchemaContextDict, 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: TildesSchemaContextDict = {"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