Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions insight/admin/user_weekly_trend_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ def render_full_preview(self, obj: UserWeeklyTrend):
{
"s_date": obj.week_start_date,
"e_date": obj.week_end_date,
"user": {
"username": obj.user.username if obj.user else "N/A",
"email": obj.user.email if obj.user else "N/A",
},
"is_expired_token_user": False,
"weekly_trend_html": weekly_trend_html,
"user_weekly_trend_html": user_weekly_trend_html,
Expand Down
1 change: 1 addition & 0 deletions insight/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class NewsletterContext:
s_date: str
e_date: str
user: dict
is_expired_token_user: bool
weekly_trend_html: str
user_weekly_trend_html: str | None = None
Expand Down
4 changes: 4 additions & 0 deletions insight/tasks/weekly_newsletter_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def _get_target_user_chunks(self) -> list[list[dict]]:
User.objects.filter(
is_active=True,
email__isnull=False,
newsletter_subscribed=True,
)
.values("id", "email", "username")
.distinct("email")
Expand Down Expand Up @@ -222,6 +223,7 @@ def _get_user_weekly_trend_html(

def _get_newsletter_html(
self,
user: dict,
is_expired_token_user: bool,
weekly_trend_html: str,
user_weekly_trend_html: str | None,
Expand All @@ -234,6 +236,7 @@ def _get_newsletter_html(
NewsletterContext(
s_date=self.weekly_info["s_date"],
e_date=self.weekly_info["e_date"],
user=user,
is_expired_token_user=is_expired_token_user,
weekly_trend_html=weekly_trend_html,
user_weekly_trend_html=user_weekly_trend_html,
Expand Down Expand Up @@ -294,6 +297,7 @@ def _build_newsletters(

# 최종 뉴스레터 렌더링
html_body = self._get_newsletter_html(
user=user,
is_expired_token_user=is_expired_token_user,
weekly_trend_html=weekly_trend_html,
user_weekly_trend_html=user_weekly_trend_html,
Expand Down
1 change: 1 addition & 0 deletions insight/tasks/weekly_user_trend_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ async def _fetch_data(
User.objects.filter(
email__isnull=False,
is_active=True,
newsletter_subscribed=True,
)
.exclude(email="")
.values("id", "username")
Expand Down
1 change: 1 addition & 0 deletions insight/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def user(db):
email="test@example.com",
username="test_user",
is_active=True,
newsletter_subscribed=True,
)


Expand Down
10 changes: 9 additions & 1 deletion insight/tests/tasks/test_weekly_newsletter_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def test_get_target_user_chunks_success(

chunks = newsletter_batch._get_target_user_chunks()

mock_filter.assert_called_once_with(
is_active=True,
email__isnull=False,
newsletter_subscribed=True,
)
assert len(chunks) == 1
assert len(chunks[0]) == 1
assert chunks[0][0]["email"] == user.email
Expand Down Expand Up @@ -197,7 +202,10 @@ def test_build_newsletters_success(
assert newsletters[0].user_id == user.id
assert newsletters[0].email_message.to[0] == user.email
# 제목 포맷 검증
assert "벨로그 대시보드 주간 뉴스레터" in newsletters[0].email_message.subject
assert (
"벨로그 대시보드 주간 뉴스레터"
in newsletters[0].email_message.subject
)

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_send_newsletters_success(
Expand Down
29 changes: 24 additions & 5 deletions insight/tests/tasks/test_weekly_newsletter_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ def test_get_user_weekly_trend_html_success(

assert trending_summary[0]["title"] in user_weekly_trend_html
assert trend_analysis["insights"] in user_weekly_trend_html
assert f'{user_weekly_stats["new_posts"]}개의 글' in user_weekly_trend_html
assert (
f'{user_weekly_stats["new_posts"]}개의 글'
in user_weekly_trend_html
)
assert "마지막으로 글을 작성하신지" not in user_weekly_trend_html
assert user.username in user_weekly_trend_html
assert "이번주에 작성한 글" in user_weekly_trend_html
Expand Down Expand Up @@ -179,13 +182,16 @@ def test_get_user_weekly_trend_html_exception(
)

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_newsletter_html_success(self, mock_logger, newsletter_batch):
def test_get_newsletter_html_success(
self, mock_logger, newsletter_batch, user
):
"""정상 사용자 뉴스레터 HTML 렌더링 테스트"""
is_expired_token_user = False
weekly_trend_html = "test-weekly-trend-html"
user_weekly_trend_html = "test-user-weekly-trend-html"

newsletter_html = newsletter_batch._get_newsletter_html(
user,
is_expired_token_user,
weekly_trend_html,
user_weekly_trend_html,
Expand All @@ -198,33 +204,45 @@ def test_get_newsletter_html_success(self, mock_logger, newsletter_batch):
assert "대시보드 보러가기" in newsletter_html
assert "Weekly Report" in newsletter_html
assert "Velog Dashboard" in newsletter_html
assert (
"user/newsletter-unsubscribe?email=" + user.email
in newsletter_html
)

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_newsletter_html_expired_token_user(
self, mock_logger, newsletter_batch
self, mock_logger, newsletter_batch, user
):
"""토큰 만료 사용자 뉴스레터 HTML 렌더링 테스트"""
is_expired_token_user = True
weekly_trend_html = "test-weekly-trend-html"
user_weekly_trend_html = "test-user-weekly-trend-html"

newsletter_html = newsletter_batch._get_newsletter_html(
user,
is_expired_token_user,
weekly_trend_html,
user_weekly_trend_html,
)

# 템플릿 렌더링 검증
assert "🚨 잠시만요, 토큰이 만료된 것 같아요!" in newsletter_html
assert "토큰이 만료되어 정상적으로 통계를 수집할 수 없었어요" in newsletter_html
assert (
"토큰이 만료되어 정상적으로 통계를 수집할 수 없었어요"
in newsletter_html
)
assert weekly_trend_html in newsletter_html
assert user_weekly_trend_html not in newsletter_html
assert "대시보드 보러가기" in newsletter_html
assert "활동 리포트" in newsletter_html
assert (
"user/newsletter-unsubscribe?email=" + user.email
in newsletter_html
)

@patch("insight.tasks.weekly_newsletter_batch.logger")
def test_get_newsletter_html_exception(
self, mock_logger, newsletter_batch
self, mock_logger, newsletter_batch, user
):
"""뉴스레터 HTML 렌더링 실패 시 예외 처리 테스트"""
with patch(
Expand All @@ -234,6 +252,7 @@ def test_get_newsletter_html_exception(

with pytest.raises(Exception):
newsletter_batch._get_newsletter_html(
user,
False,
"test-weekly-trend-html",
"test-user-weekly-trend-html",
Expand Down
2 changes: 1 addition & 1 deletion insight/tests/test_user_weekly_trend_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ def test_processed_at_formatted_with_date(
result = user_weekly_trend_admin.processed_at_formatted(
user_weekly_trend
)
assert now.strftime("%Y-%m-%d %H:%M") == result
assert now.strftime("%Y-%m-%d %H:%M:%S") == result
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이 부분에 초 단위까지 표시하게 바뀐 이유가 뭘까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지난 현우님 핫픽스의 사이드이펙트입니다~! 테스트가 실패해서 그냥 고쳐두었습니다.


def test_processed_at_formatted_no_date(
self, user_weekly_trend_admin, user_weekly_trend
Expand Down
2 changes: 1 addition & 1 deletion insight/tests/test_weekly_trend_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_processed_at_formatted_with_date(
weekly_trend.save()

result = weekly_trend_admin.processed_at_formatted(weekly_trend)
assert now.strftime("%Y-%m-%d %H:%M") == result
assert now.strftime("%Y-%m-%d %H:%M:%S") == result

def test_processed_at_formatted_no_date(
self, weekly_trend_admin, weekly_trend: WeeklyTrend
Expand Down
48 changes: 32 additions & 16 deletions templates/insights/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,29 +134,30 @@
{{weekly_trend_html}}
{% endif %}

<h2
style="
box-sizing: border-box;
margin-top: 40px;
color: #000000;
font-size: 24px;
font-weight: 900;
letter-spacing: 0;
"
>
{% if user.username %}
{{user.username}}님의 활동 리포트
{% else %}
활동 리포트
{% endif %}
</h2>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개인적으로 궁금해서 님기는 코멘트이니 편하게 답변 부탁드려요!!
해당 부분이 user_weekly_trend.html에서 index.html로 이동한 이유가 궁금합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지난번에 템플릿 수정이후로 저게 중복되어서 들어가 있더라고요. (index.html 에도, user_weekly_trend.html 에도)
그래서 하나를 삭제해야 했는데, 유저 개인 트렌드가 없어도 토큰만료 박스는 활동 리포트 하단에 있는게 나을 거라고 판단해서 index.html 위치에 있는 것을 살렸습니다!

{% if not is_expired_token_user and user_weekly_trend_html %}
{{user_weekly_trend_html}}
{% endif %}

{% if is_expired_token_user %}
<!-- Token Expired Warning -->
<div style="margin-bottom: 40px; box-sizing: border-box">
<h2
style="
font-size: 24px;
font-weight: 900;
color: #000000;
margin-bottom: 20px;
letter-spacing: 0;
box-sizing: border-box;
"
>
{% if user.username %}
{{user.username}}님의 활동 리포트
{% else %}
활동 리포트
{% endif %}
</h2>
<div
style="
background-color: #fffbd7;
Expand Down Expand Up @@ -311,6 +312,21 @@
>
개인정보처리방침
</a>
&nbsp;&nbsp;|&nbsp;&nbsp;
<!-- 뉴스레터 구독 해제: API 엔드포인트로 직접 구독 해제 처리 -->
<a
href="https://velog-dashboard.kro.kr/api/user/newsletter-unsubscribe?email={{user.email}}"
target="_blank"
rel="noopener noreferrer"
style="
color: #4d4d4d;
text-decoration: underline;
box-sizing: border-box;
display: inline-block;
"
>
수신 거부
</a>
</p>
</td>
</tr>
Expand Down
20 changes: 1 addition & 19 deletions templates/insights/user_weekly_trend.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
<div
class="user-weekly-trend"
style="box-sizing: border-box; margin-top: 40px; color: #000000"
style="box-sizing: border-box; color: #000000"
>
{% if insight.trending_summary or insight.user_weekly_stats or insight.user_weekly_reminder %}
<h2
style="
box-sizing: border-box;
margin-bottom: 24px;
color: #000000;
font-size: 24px;
font-weight: 900;
letter-spacing: 0;
"
>
{% if user.username %}
{{user.username}}님의 활동 리포트
{% else %}
활동 리포트
{% endif %}
</h2>
{% endif %}
{% if insight.user_weekly_stats %}
<div
style="
Expand Down
18 changes: 17 additions & 1 deletion users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class UserAdmin(admin.ModelAdmin):
"email",
"group_id",
"is_active",
"newsletter_subscribed",
"created_at",
"post_count",
"get_qr_login_token",
Expand All @@ -31,7 +32,7 @@ class UserAdmin(admin.ModelAdmin):
empty_value_display = "-"
ordering = ["-created_at"]

actions = ["make_inactive", "update_stats"]
actions = ["make_inactive", "update_stats", "make_unsubscribed"]

def get_list_display(self, request):
list_display = super().get_list_display(request)
Expand All @@ -40,6 +41,7 @@ def get_list_display(self, request):
"email": "이메일",
"group_id": "그룹 ID",
"is_active": "활성화 여부",
"newsletter_subscribed": "뉴스레터 구독 여부",
"created_at": "생성일",
}
return list_display
Expand Down Expand Up @@ -95,6 +97,20 @@ def make_inactive(self, request: HttpRequest, queryset: QuerySet[User]):
messages.SUCCESS,
)

@admin.action(description="선택된 사용자 뉴스레터 구독 해제")
def make_unsubscribed(
self, request: HttpRequest, queryset: QuerySet[User]
):
updated = queryset.update(newsletter_subscribed=False)
logger.info(
f"{request.user} 가 {updated} 명 사용자를 뉴스레터 구독 해제 했습니다."
)
self.message_user(
request,
f"{updated} 명의 사용자가 뉴스레터 구독 해제되었습니다.",
messages.SUCCESS,
)

@admin.action(
description="선택된 사용자 실시간 통계 업데이트 (1명 정도만 진행, 이상 timeout 발생 위험)"
)
Expand Down
19 changes: 19 additions & 0 deletions users/migrations/0014_user_newsletter_subscribed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 5.1.6 on 2025-10-01 17:57

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("users", "0013_user_thumbnail"),
]

operations = [
migrations.AddField(
model_name="user",
name="newsletter_subscribed",
field=models.BooleanField(
default=True, verbose_name="뉴스레터 구독 여부"
),
),
]
3 changes: 3 additions & 0 deletions users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class User(TimeStampedModel):
is_active = models.BooleanField(
default=True, null=False, verbose_name="활성 여부"
)
newsletter_subscribed = models.BooleanField(
default=True, null=False, verbose_name="뉴스레터 구독 여부"
)

def __str__(self) -> str:
return f"{self.velog_uuid}"
Expand Down