journal: multi-select emotions and per-category writing templates
- Mood switches from a single enum column to a many-to-many JournalEntryMood table so an entry can carry several feelings at once; the vocabulary is trimmed to 9 named emotions (dropped the overlapping satisfaction scale) with unified noun-style labels. - Categories can define a content_template that pre-fills the "new entry" textarea when selected (only if the user hasn't started typing), seeded with a Story/Feelings/Decisions/Insights/Actions reflection template on the default "일상" category. - Fixes a real attribute-injection bug found while building the template feature: Jinja's built-in |tojson filter doesn't escape double quotes, which breaks a double-quoted x-data="..." attribute when the JSON payload contains one; added |forceescape and a regression test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,9 @@ class JournalCategory(Base):
|
|||||||
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
name: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||||
color: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
color: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||||
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
sort_order: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||||
|
# 이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워주는 틀(예: Story/Feelings/Decisions/... 회고 양식).
|
||||||
|
# 사용자가 직접 타이핑을 시작하면 더 이상 덮어쓰지 않는다(프론트에서 처리).
|
||||||
|
content_template: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False)
|
created_at: Mapped[datetime] = mapped_column(server_default=func.now(), nullable=False)
|
||||||
|
|
||||||
entries: Mapped[list["JournalEntry"]] = relationship(
|
entries: Mapped[list["JournalEntry"]] = relationship(
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ JOURNAL_MOOD_OPTIONS = [
|
|||||||
]
|
]
|
||||||
templates.env.globals["journal_mood_options"] = JOURNAL_MOOD_OPTIONS
|
templates.env.globals["journal_mood_options"] = JOURNAL_MOOD_OPTIONS
|
||||||
templates.env.globals["journal_mood_emoji"] = {m.value: emoji for m, emoji, _ in JOURNAL_MOOD_OPTIONS}
|
templates.env.globals["journal_mood_emoji"] = {m.value: emoji for m, emoji, _ in JOURNAL_MOOD_OPTIONS}
|
||||||
|
# Jinja2 내장 |tojson 필터가 이 policy를 읽어서 json.dumps에 넘긴다 — 기본값(ensure_ascii=True)이면
|
||||||
|
# 한글이 \uXXXX로 이스케이프돼 응답 본문에서 읽기 힘들어진다.
|
||||||
|
templates.env.policies["json.dumps_kwargs"] = {"ensure_ascii": False}
|
||||||
|
|
||||||
|
|
||||||
def _parse_moods(raw: str) -> list[JournalMood]:
|
def _parse_moods(raw: str) -> list[JournalMood]:
|
||||||
@@ -111,6 +114,7 @@ def journal_page(
|
|||||||
"tab": tab,
|
"tab": tab,
|
||||||
"categories": categories,
|
"categories": categories,
|
||||||
"category_id": category_id,
|
"category_id": category_id,
|
||||||
|
"category_templates": {str(c.id): c.content_template or "" for c in categories},
|
||||||
"entry_counts": journal_service.count_entries_by_category(db, current.id) if tab == "manage" else {},
|
"entry_counts": journal_service.count_entries_by_category(db, current.id) if tab == "manage" else {},
|
||||||
"on_this_day": journal_service.get_on_this_day(db, current.id, today),
|
"on_this_day": journal_service.get_on_this_day(db, current.id, today),
|
||||||
"prompt": journal_service.get_random_prompt(db),
|
"prompt": journal_service.get_random_prompt(db),
|
||||||
@@ -274,14 +278,18 @@ def delete_attachment_page(
|
|||||||
|
|
||||||
@router.post("/journal/categories/new")
|
@router.post("/journal/categories/new")
|
||||||
def create_category_page(
|
def create_category_page(
|
||||||
request: Request, name: str = Form(""), color: str | None = Form(None), db: Session = Depends(get_db)
|
request: Request,
|
||||||
|
name: str = Form(""),
|
||||||
|
color: str | None = Form(None),
|
||||||
|
content_template: str | None = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
current = _current_user_or_redirect(request, db)
|
current = _current_user_or_redirect(request, db)
|
||||||
if isinstance(current, RedirectResponse):
|
if isinstance(current, RedirectResponse):
|
||||||
return current
|
return current
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = JournalCategoryCreate(name=name, color=color)
|
data = JournalCategoryCreate(name=name, color=color, content_template=content_template)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
message = exc.errors()[0]["msg"].removeprefix("Value error, ")
|
message = exc.errors()[0]["msg"].removeprefix("Value error, ")
|
||||||
return HTMLResponse(message)
|
return HTMLResponse(message)
|
||||||
@@ -303,6 +311,7 @@ def edit_category_page(
|
|||||||
category_id: int,
|
category_id: int,
|
||||||
name: str = Form(""),
|
name: str = Form(""),
|
||||||
color: str | None = Form(None),
|
color: str | None = Form(None),
|
||||||
|
content_template: str | None = Form(None),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
current = _current_user_or_redirect(request, db)
|
current = _current_user_or_redirect(request, db)
|
||||||
@@ -314,7 +323,7 @@ def edit_category_page(
|
|||||||
return Response(status_code=404)
|
return Response(status_code=404)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = JournalCategoryCreate(name=name, color=color)
|
data = JournalCategoryCreate(name=name, color=color, content_template=content_template)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
message = exc.errors()[0]["msg"].removeprefix("Value error, ")
|
message = exc.errors()[0]["msg"].removeprefix("Value error, ")
|
||||||
return HTMLResponse(message)
|
return HTMLResponse(message)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from app.models.journal import JournalAttachmentType, JournalMood
|
|||||||
class JournalCategoryBase(BaseModel):
|
class JournalCategoryBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
color: str | None = None
|
color: str | None = None
|
||||||
|
content_template: str | None = None
|
||||||
|
|
||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -25,6 +26,14 @@ class JournalCategoryBase(BaseModel):
|
|||||||
v = v.strip()
|
v = v.strip()
|
||||||
return v or None
|
return v or None
|
||||||
|
|
||||||
|
@field_validator("content_template")
|
||||||
|
@classmethod
|
||||||
|
def blank_template_to_none(cls, v: str | None) -> str | None:
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
v = v.strip()
|
||||||
|
return v or None
|
||||||
|
|
||||||
|
|
||||||
class JournalCategoryCreate(JournalCategoryBase):
|
class JournalCategoryCreate(JournalCategoryBase):
|
||||||
pass
|
pass
|
||||||
@@ -36,6 +45,7 @@ class JournalCategoryOut(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
color: str | None
|
color: str | None
|
||||||
|
content_template: str | None
|
||||||
sort_order: int | None
|
sort_order: int | None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,27 @@ ALLOWED_IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
|
|||||||
ALLOWED_VIDEO_TYPES = {"video/mp4", "video/quicktime"}
|
ALLOWED_VIDEO_TYPES = {"video/mp4", "video/quicktime"}
|
||||||
THUMBNAIL_WIDTH = 400
|
THUMBNAIL_WIDTH = 400
|
||||||
DEFAULT_CATEGORY_NAME = "일상"
|
DEFAULT_CATEGORY_NAME = "일상"
|
||||||
|
DEFAULT_CATEGORY_TEMPLATE = """**1. Story : 오늘 무슨 일이 있었나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**2. Feelings : 오늘 들었던 생각과 나의 감정은?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**3. Decisions : 오늘 내가 내린 결정이 있나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**4. Insights : 나에 대해 새롭게 알게된 사실이 있나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
→ 나에 대한 인사이트는 메타인지를 키워주는 **소중한 기록**입니다. 꼭 보관하세요.
|
||||||
|
|
||||||
|
**5. Actions : 내가 다음에 할 행동은 무엇인가요?**
|
||||||
|
|
||||||
|
- """
|
||||||
|
|
||||||
|
|
||||||
# ---- 카테고리 ----
|
# ---- 카테고리 ----
|
||||||
@@ -54,7 +75,9 @@ def get_category(db: Session, category_id: int, user_id: int) -> JournalCategory
|
|||||||
|
|
||||||
|
|
||||||
def create_category(db: Session, user_id: int, data: JournalCategoryCreate) -> JournalCategory:
|
def create_category(db: Session, user_id: int, data: JournalCategoryCreate) -> JournalCategory:
|
||||||
category = JournalCategory(user_id=user_id, name=data.name, color=data.color)
|
category = JournalCategory(
|
||||||
|
user_id=user_id, name=data.name, color=data.color, content_template=data.content_template
|
||||||
|
)
|
||||||
db.add(category)
|
db.add(category)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(category)
|
db.refresh(category)
|
||||||
@@ -64,6 +87,7 @@ def create_category(db: Session, user_id: int, data: JournalCategoryCreate) -> J
|
|||||||
def update_category(db: Session, category: JournalCategory, data: JournalCategoryCreate) -> JournalCategory:
|
def update_category(db: Session, category: JournalCategory, data: JournalCategoryCreate) -> JournalCategory:
|
||||||
category.name = data.name
|
category.name = data.name
|
||||||
category.color = data.color
|
category.color = data.color
|
||||||
|
category.content_template = data.content_template
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(category)
|
db.refresh(category)
|
||||||
return category
|
return category
|
||||||
@@ -79,7 +103,9 @@ def ensure_default_category(db: Session, user_id: int) -> JournalCategory:
|
|||||||
)
|
)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
return existing
|
return existing
|
||||||
return create_category(db, user_id, JournalCategoryCreate(name=DEFAULT_CATEGORY_NAME))
|
return create_category(
|
||||||
|
db, user_id, JournalCategoryCreate(name=DEFAULT_CATEGORY_NAME, content_template=DEFAULT_CATEGORY_TEMPLATE)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def reorder_categories(db: Session, user_id: int, ordered_ids: list[int]) -> None:
|
def reorder_categories(db: Session, user_id: int, ordered_ids: list[int]) -> None:
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
<label for="edit-category-color-{{ category.id }}">색상</label>
|
<label for="edit-category-color-{{ category.id }}">색상</label>
|
||||||
<input type="color" id="edit-category-color-{{ category.id }}" name="color" value="{{ category.color or '#d97757' }}" />
|
<input type="color" id="edit-category-color-{{ category.id }}" name="color" value="{{ category.color or '#d97757' }}" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="edit-category-template-{{ category.id }}">기본 작성 틀 (선택)</label>
|
||||||
|
<textarea id="edit-category-template-{{ category.id }}" name="content_template" rows="4" placeholder="이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워줄 틀을 입력하세요">{{ category.content_template or '' }}</textarea>
|
||||||
|
</div>
|
||||||
<div id="journal-category-edit-error-{{ category.id }}" class="form-error-text"></div>
|
<div id="journal-category-edit-error-{{ category.id }}" class="form-error-text"></div>
|
||||||
<div style="display:flex; gap:8px;">
|
<div style="display:flex; gap:8px;">
|
||||||
<button type="submit" class="btn btn-primary" style="flex:1;">저장</button>
|
<button type="submit" class="btn btn-primary" style="flex:1;">저장</button>
|
||||||
@@ -85,6 +89,10 @@
|
|||||||
<label for="new-category-color">색상 (선택)</label>
|
<label for="new-category-color">색상 (선택)</label>
|
||||||
<input type="color" id="new-category-color" name="color" value="#d97757" />
|
<input type="color" id="new-category-color" name="color" value="#d97757" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="new-category-template">기본 작성 틀 (선택)</label>
|
||||||
|
<textarea id="new-category-template" name="content_template" rows="4" placeholder="이 카테고리로 새 기록을 쓸 때 내용칸에 미리 채워줄 틀을 입력하세요"></textarea>
|
||||||
|
</div>
|
||||||
<div id="journal-category-error" class="form-error-text"></div>
|
<div id="journal-category-error" class="form-error-text"></div>
|
||||||
<button type="submit" class="btn btn-primary btn-block">추가하기</button>
|
<button type="submit" class="btn btn-primary btn-block">추가하기</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -113,6 +121,17 @@
|
|||||||
open: false,
|
open: false,
|
||||||
moods: [],
|
moods: [],
|
||||||
toggleMood(v) { this.moods.includes(v) ? this.moods = this.moods.filter(m => m !== v) : this.moods.push(v) },
|
toggleMood(v) { this.moods.includes(v) ? this.moods = this.moods.filter(m => m !== v) : this.moods.push(v) },
|
||||||
|
categoryTemplates: {{ category_templates|tojson|forceescape }},
|
||||||
|
content: '',
|
||||||
|
lastAutoFilled: '',
|
||||||
|
onCategoryChange(categoryId) {
|
||||||
|
const tmpl = this.categoryTemplates[categoryId] || '';
|
||||||
|
if (this.content === this.lastAutoFilled) {
|
||||||
|
this.content = tmpl;
|
||||||
|
this.lastAutoFilled = tmpl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() { this.onCategoryChange(this.$refs.categorySelect.value); },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-primary btn-block" @click="open = !open">
|
<button type="button" class="btn btn-primary btn-block" @click="open = !open">
|
||||||
@@ -135,7 +154,7 @@
|
|||||||
>
|
>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="new-entry-category">카테고리</label>
|
<label for="new-entry-category">카테고리</label>
|
||||||
<select id="new-entry-category" name="category_id">
|
<select id="new-entry-category" name="category_id" x-ref="categorySelect" @change="onCategoryChange($event.target.value)">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<option value="{{ category.id }}" {{ 'selected' if category_id == category.id }}>{{ category.name }}</option>
|
<option value="{{ category.id }}" {{ 'selected' if category_id == category.id }}>{{ category.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -154,7 +173,7 @@
|
|||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="new-entry-content">내용</label>
|
<label for="new-entry-content">내용</label>
|
||||||
<textarea id="new-entry-content" name="content" rows="5" required placeholder="오늘 하루는 어땠나요?"></textarea>
|
<textarea id="new-entry-content" name="content" rows="8" required placeholder="오늘 하루는 어땠나요?" x-model="content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""add content_template to journal_category, seed the default 일상 category's template
|
||||||
|
|
||||||
|
Revision ID: 0013_journal_category_template
|
||||||
|
Revises: 0012_remove_satisfaction_moods
|
||||||
|
Create Date: 2026-08-04
|
||||||
|
|
||||||
|
카테고리별로 "새 기록 쓰기" 폼의 내용칸을 미리 채워주는 틀(예: Story/Feelings/Decisions/
|
||||||
|
Insights/Actions 회고 양식)을 지정할 수 있게 한다. 이미 존재하는 "일상" 카테고리에는 이 틀을
|
||||||
|
바로 채워 넣는다(새로 자동 생성되는 "일상"은 ensure_default_category가 채운다).
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0013_journal_category_template"
|
||||||
|
down_revision: Union[str, None] = "0012_remove_satisfaction_moods"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
DAILY_TEMPLATE = """**1. Story : 오늘 무슨 일이 있었나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**2. Feelings : 오늘 들었던 생각과 나의 감정은?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**3. Decisions : 오늘 내가 내린 결정이 있나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
**4. Insights : 나에 대해 새롭게 알게된 사실이 있나요?**
|
||||||
|
|
||||||
|
-
|
||||||
|
|
||||||
|
→ 나에 대한 인사이트는 메타인지를 키워주는 **소중한 기록**입니다. 꼭 보관하세요.
|
||||||
|
|
||||||
|
**5. Actions : 내가 다음에 할 행동은 무엇인가요?**
|
||||||
|
|
||||||
|
- """
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("journal_category", sa.Column("content_template", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
journal_category = sa.table(
|
||||||
|
"journal_category", sa.column("name", sa.String), sa.column("content_template", sa.Text)
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
journal_category.update()
|
||||||
|
.where(journal_category.c.name == "일상")
|
||||||
|
.values(content_template=DAILY_TEMPLATE)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("journal_category", "content_template")
|
||||||
@@ -41,12 +41,25 @@ def _upload(filename: str, content_type: str, data: bytes) -> UploadFile:
|
|||||||
def test_ensure_default_category_creates_once(db_session, test_user):
|
def test_ensure_default_category_creates_once(db_session, test_user):
|
||||||
first = journal_service.ensure_default_category(db_session, test_user.id)
|
first = journal_service.ensure_default_category(db_session, test_user.id)
|
||||||
assert first.name == journal_service.DEFAULT_CATEGORY_NAME
|
assert first.name == journal_service.DEFAULT_CATEGORY_NAME
|
||||||
|
assert first.content_template == journal_service.DEFAULT_CATEGORY_TEMPLATE.strip()
|
||||||
|
|
||||||
second = journal_service.ensure_default_category(db_session, test_user.id)
|
second = journal_service.ensure_default_category(db_session, test_user.id)
|
||||||
assert second.id == first.id
|
assert second.id == first.id
|
||||||
assert len(journal_service.list_categories(db_session, test_user.id)) == 1
|
assert len(journal_service.list_categories(db_session, test_user.id)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_category_content_template_persists_and_updates(db_session, test_user):
|
||||||
|
category = journal_service.create_category(
|
||||||
|
db_session, test_user.id, JournalCategoryCreate(name="회고", content_template="질문 1\n\n질문 2")
|
||||||
|
)
|
||||||
|
assert category.content_template == "질문 1\n\n질문 2"
|
||||||
|
|
||||||
|
updated = journal_service.update_category(
|
||||||
|
db_session, category, JournalCategoryCreate(name="회고", content_template=" ")
|
||||||
|
)
|
||||||
|
assert updated.content_template is None
|
||||||
|
|
||||||
|
|
||||||
def test_list_categories_scoped_to_user(db_session, test_user, other_user):
|
def test_list_categories_scoped_to_user(db_session, test_user, other_user):
|
||||||
_make_category(db_session, test_user.id, name="일상")
|
_make_category(db_session, test_user.id, name="일상")
|
||||||
_make_category(db_session, other_user.id, name="남의 카테고리")
|
_make_category(db_session, other_user.id, name="남의 카테고리")
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from app.schemas.journal import JournalCategoryCreate, JournalEntryCreate
|
|||||||
from app.services import journal_service
|
from app.services import journal_service
|
||||||
|
|
||||||
|
|
||||||
def _make_category(db_session, user_id, name="일상"):
|
def _make_category(db_session, user_id, name="일상", content_template=None):
|
||||||
return journal_service.create_category(db_session, user_id, JournalCategoryCreate(name=name))
|
return journal_service.create_category(
|
||||||
|
db_session, user_id, JournalCategoryCreate(name=name, content_template=content_template)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_entry(db_session, user_id, category_id, entry_date=None, content="기록"):
|
def _make_entry(db_session, user_id, category_id, entry_date=None, content="기록"):
|
||||||
@@ -36,6 +38,29 @@ def test_journal_page_creates_default_category_and_renders(auth_client):
|
|||||||
assert journal_service.DEFAULT_CATEGORY_NAME in response.text
|
assert journal_service.DEFAULT_CATEGORY_NAME in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_journal_page_embeds_category_content_template(auth_client):
|
||||||
|
response = auth_client.get("/journal")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "Story" in response.text
|
||||||
|
assert "메타인지" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_journal_page_template_with_double_quotes_does_not_break_x_data_attribute(
|
||||||
|
auth_client, db_session, test_user
|
||||||
|
):
|
||||||
|
baseline_form_count = auth_client.get("/journal").text.count("<form")
|
||||||
|
|
||||||
|
_make_category(db_session, test_user.id, name="따옴표카테고리", content_template='내용에 "큰따옴표"가 있어요')
|
||||||
|
|
||||||
|
response = auth_client.get("/journal")
|
||||||
|
assert response.status_code == 200
|
||||||
|
# x-data="..." 속성 안에 이스케이프 안 된 "가 섞이면 속성이 거기서 끊겨 뒤 마크업이 전부
|
||||||
|
# 속성값으로 흡수되는데, 그러면 폼의 <form 태그들이 열린 속성값 텍스트에 파묻혀 개수가
|
||||||
|
# 줄어든다 — 카테고리 하나 늘었다고 <form 개수가 그대로인지로 attribute-injection 여부를 검증한다.
|
||||||
|
assert response.text.count("<form") == baseline_form_count
|
||||||
|
assert "\\"큰따옴표\\"" in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_create_entry_via_multipart_form(auth_client, db_session, test_user, tmp_path, monkeypatch):
|
def test_create_entry_via_multipart_form(auth_client, db_session, test_user, tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path))
|
monkeypatch.setattr(journal_service.settings, "journal_media_root", str(tmp_path))
|
||||||
category = _make_category(db_session, test_user.id, name="투자")
|
category = _make_category(db_session, test_user.id, name="투자")
|
||||||
|
|||||||
Reference in New Issue
Block a user