From 18a649f30527d6c5e56263290d07ce454250d8a5 Mon Sep 17 00:00:00 2001 From: dendencat Date: Tue, 16 Sep 2025 12:02:07 +0900 Subject: [PATCH 1/2] feat: enhance article editing with unique slug generation and improved UI for editing and publishing --- requirements.txt | 1 + techblog_cms/models.py | 13 +++- techblog_cms/templates/article_detail.html | 5 +- techblog_cms/templates/article_editor.html | 11 +-- techblog_cms/templates/dashboard.html | 4 ++ techblog_cms/templatetags/markdown_filter.py | 5 +- techblog_cms/tests/test_article_editing.py | 74 ++++++++++++++++++++ techblog_cms/urls.py | 1 + techblog_cms/views.py | 70 ++++++++++-------- 9 files changed, 145 insertions(+), 39 deletions(-) create mode 100644 techblog_cms/tests/test_article_editing.py diff --git a/requirements.txt b/requirements.txt index 9132163..96b047f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,6 +42,7 @@ isort>=5.13,<6.0 # Documentation markdown>=3.5,<4.0 +linkify-it-py>=2.0,<3.0 # Optional: Image processing # Pillow>=10.1,<11.0 diff --git a/techblog_cms/models.py b/techblog_cms/models.py index 04c9038..1f65f32 100644 --- a/techblog_cms/models.py +++ b/techblog_cms/models.py @@ -1,3 +1,4 @@ +import uuid from django.db import models from django.utils.text import slugify from django.urls import reverse @@ -51,9 +52,17 @@ class Article(models.Model): tags = models.ManyToManyField(Tag, blank=True) image = models.ImageField(upload_to='articles/', blank=True, null=True) + def _generate_unique_slug(self): + base_slug = slugify(self.title) or 'article' + while True: + hash_fragment = uuid.uuid4().hex[:8] + candidate = f"{base_slug}-{hash_fragment}" + if not Article.objects.filter(slug=candidate).exclude(pk=self.pk).exists(): + return candidate + def save(self, *args, **kwargs): if not self.slug: - self.slug = slugify(self.title) + self.slug = self._generate_unique_slug() if not self.excerpt and self.content: self.excerpt = self.content[:200] super().save(*args, **kwargs) @@ -62,4 +71,4 @@ def __str__(self): return self.title def get_absolute_url(self): - return reverse('article', kwargs={'slug': self.slug}) + return reverse('article_detail', kwargs={'slug': self.slug}) diff --git a/techblog_cms/templates/article_detail.html b/techblog_cms/templates/article_detail.html index 617915e..0feb26e 100644 --- a/techblog_cms/templates/article_detail.html +++ b/techblog_cms/templates/article_detail.html @@ -226,7 +226,10 @@

{{ article.title }}

- Published: {{ article.created_at|date:"M d, Y" }} + Published: {{ article.created_at|date:"M d, Y H:i" }} + {% if article.updated_at and article.updated_at != article.created_at %} + Updated: {{ article.updated_at|date:"M d, Y H:i" }} + {% endif %} {% if article.category %} Category: diff --git a/techblog_cms/templates/article_editor.html b/techblog_cms/templates/article_editor.html index 37d0e20..b9c5fb2 100644 --- a/techblog_cms/templates/article_editor.html +++ b/techblog_cms/templates/article_editor.html @@ -5,13 +5,13 @@
+ + 編集 + {% if not a.published %} 下書き {% endif %} diff --git a/techblog_cms/templatetags/markdown_filter.py b/techblog_cms/templatetags/markdown_filter.py index d6102e6..830d2e9 100644 --- a/techblog_cms/templatetags/markdown_filter.py +++ b/techblog_cms/templatetags/markdown_filter.py @@ -16,9 +16,10 @@ def markdown_to_html(text): html = markdown.markdown(text, extensions=[ 'extra', # Extra features like tables, footnotes, and raw HTML 'codehilite', # Code highlighting with Pygments - 'toc', # Table of contents + 'toc', # Table of contents 'fenced_code', # Fenced code blocks - 'nl2br', # Convert newlines to
+ 'nl2br', # Convert newlines to
+ 'linkify', # Auto-link plain URLs ], extension_configs={ 'codehilite': { 'linenums': False, # Disable line numbers diff --git a/techblog_cms/tests/test_article_editing.py b/techblog_cms/tests/test_article_editing.py new file mode 100644 index 0000000..c86ba53 --- /dev/null +++ b/techblog_cms/tests/test_article_editing.py @@ -0,0 +1,74 @@ +from django.test import TestCase +from django.contrib.auth.models import User +from django.urls import reverse + +from techblog_cms.models import Article, Category +from techblog_cms.templatetags.markdown_filter import markdown_to_html + + +class ArticleSlugTests(TestCase): + def setUp(self): + self.category = Category.objects.create(name="General", description="General articles") + + def test_slug_contains_hash_fragment(self): + article = Article.objects.create( + title="Example Post", + content="Body", + category=self.category, + published=False, + ) + self.assertRegex(article.slug, r"^example-post-[0-9a-f]{8}$") + + def test_duplicate_titles_generate_unique_slugs(self): + first = Article.objects.create( + title="Example Post", + content="Body", + category=self.category, + ) + second = Article.objects.create( + title="Example Post", + content="Body", + category=self.category, + ) + self.assertNotEqual(first.slug, second.slug) + + +class ArticleEditingTests(TestCase): + def setUp(self): + self.category = Category.objects.create(name="General", description="General articles") + self.article = Article.objects.create( + title="Original Title", + content="Original content", + category=self.category, + published=False, + ) + self.user = User.objects.create_user(username="editor", password="pass1234") + + def test_edit_updates_content_and_preserves_slug(self): + self.client.login(username="editor", password="pass1234") + url = reverse("article_edit", args=[self.article.slug]) + response = self.client.post( + url, + { + "title": "Original Title", + "content": "Updated body with new info", + "action": "publish", + }, + ) + self.assertEqual(response.status_code, 302) + + refreshed = Article.objects.get(pk=self.article.pk) + self.assertEqual(refreshed.content, "Updated body with new info") + self.assertTrue(refreshed.published) + self.assertEqual(refreshed.slug, self.article.slug) + self.assertGreaterEqual(refreshed.updated_at, refreshed.created_at) + + detail_response = self.client.get(reverse("article_detail", args=[refreshed.slug])) + self.assertContains(detail_response, "Updated body with new info") + self.assertContains(detail_response, "Updated:") + + +class MarkdownRenderingTests(TestCase): + def test_plain_urls_are_linkified(self): + html = markdown_to_html("Check https://example.com for details") + self.assertIn('https://example.com', html) diff --git a/techblog_cms/urls.py b/techblog_cms/urls.py index cd7ac80..91e6ad8 100644 --- a/techblog_cms/urls.py +++ b/techblog_cms/urls.py @@ -16,6 +16,7 @@ path('logout/', views.logout_view, name='logout'), path('dashboard/', views.dashboard_view, name='dashboard'), path('dashboard/articles/new/', views.article_editor_view, name='article_new'), + re_path(r'^dashboard/articles/(?P[\w\-]+)/edit/$', views.article_editor_view, name='article_edit'), re_path(r'^dashboard/articles/(?P[\w\-]+)/delete/$', views.article_delete_view, name='article_delete'), path('dashboard/articles/delete/success/', views.article_delete_success_view, name='article_delete_success'), path('api/health/', views.health_check, name='health_check'), diff --git a/techblog_cms/views.py b/techblog_cms/views.py index b721436..cd4458f 100644 --- a/techblog_cms/views.py +++ b/techblog_cms/views.py @@ -125,40 +125,52 @@ def article_delete_success_view(request): @login_required @require_http_methods(["GET", "POST"]) -def article_editor_view(request): +def article_editor_view(request, slug=None): + article = get_object_or_404(Article, slug=slug) if slug else None + if request.method == 'POST': - title = request.POST.get('title') - content = request.POST.get('content') + title = (request.POST.get('title') or '').strip() + content = (request.POST.get('content') or '').strip() action = request.POST.get('action') # 'save' or 'publish' - + if not title or not content: - return render(request, 'article_editor.html', {"error": "タイトルと本文は必須です。"}) - - # デフォルトのカテゴリを取得(存在しない場合は作成) - category, created = Category.objects.get_or_create( - name='General', - defaults={'description': 'General articles'} - ) - - # ユニークなslugを生成 - base_slug = title.lower().replace(' ', '-').replace('/', '-') - slug = base_slug - counter = 1 - while Article.objects.filter(slug=slug).exists(): - slug = f"{base_slug}-{counter}" - counter += 1 - - # 記事作成時にカテゴリを指定 + context = { + "error": "タイトルと本文は必須です。", + "article": article, + "title_value": title, + "content_value": content, + } + return render(request, 'article_editor.html', context) + published = action == 'publish' - article = Article.objects.create( - title=title, - content=content, - slug=slug, - category=category, - published=published # アクションに応じて公開状態を設定 - ) + + if article: + article.title = title + article.content = content + if published: + article.published = True + article.save() + else: + category, _ = Category.objects.get_or_create( + name='General', + defaults={'description': 'General articles'} + ) + article = Article( + title=title, + content=content, + category=category, + published=published, + ) + article.save() + return redirect('dashboard') - return render(request, 'article_editor.html') + + context = { + 'article': article, + 'title_value': getattr(article, 'title', ''), + 'content_value': getattr(article, 'content', ''), + } + return render(request, 'article_editor.html', context) @login_required From 912837af1e2eb253862a6940cdc80cffa47fc0cb Mon Sep 17 00:00:00 2001 From: dendencat Date: Tue, 16 Sep 2025 17:34:28 +0900 Subject: [PATCH 2/2] =?UTF-8?q?AGENTS.md=E3=82=92=E6=97=A5=E6=9C=AC?= =?UTF-8?q?=E8=AA=9E=E3=81=8B=E3=82=89=E8=8B=B1=E8=AA=9E=E3=81=AB=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 200 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 102 insertions(+), 98 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 016b300..0b16fa8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,108 +1,112 @@ -# AGENTS.md - AIアシスタント向けガイド - -## 目的 -このドキュメントは、GitHub CopilotやClaudeなどのAIアシスタントがこのレポジトリ(techblog_cms)を理解し、効果的に支援するためのガイドラインを提供します。レポジトリの構造、技術スタック、開発プロセスを明確にし、AIが適切なコード生成やアドバイスを行えるようにします。 - -## レポジトリ概要 -techblog_cmsは、Djangoベースの技術ブログコンテンツ管理システムです。Docker Composeを使用したコンテナ化環境で、Nginxによるリバースプロキシ、PostgreSQLデータベース、Redisキャッシュ、Let's EncryptによるSSL証明書の自動管理を統合しています。プロダクションレディな構成を目指し、セキュリティとスケーラビリティを重視しています。 - -## 技術スタック -- **バックエンド**: Django 4.2, Python 3.11 -- **データベース**: PostgreSQL -- **キャッシュ**: Redis -- **Webサーバー**: Nginx (リバースプロキシ + 静的ファイル配信) -- **コンテナ化**: Docker, Docker Compose -- **SSL証明書**: Let's Encrypt (Certbot) -- **テスト**: pytest, Django Test Framework +# AGENTS.md - Guide for AI Assistants + +## Purpose +This document provides guidelines for AI assistants like GitHub Copilot and Claude to understand this repository (techblog_cms) and assist effectively. It clarifies the repository structure, tech stack, and development process to enable AI to generate appropriate code and provide sound advice. + +## Repository Overview +techblog_cms is a Django-based technical blog content management system. It operates in a containerized environment using Docker Compose, integrating Nginx reverse proxy, PostgreSQL database, Redis cache, and automated SSL certificate management via Let's Encrypt. The configuration prioritizes security and scalability, aiming for production readiness. + +## Technology Stack +- **Backend**: Django 4.2, Python 3.11 +- **Database**: PostgreSQL +- **Cache**: Redis +- **Web Server**: Nginx (Reverse Proxy + Static File Delivery) +- **Containerization**: Docker, Docker Compose +- **SSL Certificates**: Let's Encrypt (Certbot) +- **Testing**: pytest, Django Test Framework - **CI/CD**: GitHub Actions -- **セキュリティ**: HTTPS強制, セキュリティヘッダー, 環境変数管理 -- **フロントエンド**: HTML/CSS (Tailwind CSS), JavaScript (最小限) +- **Security**: HTTPS enforcement, security headers, environment variable management +- **Frontend**: HTML/CSS (Tailwind CSS), JavaScript (minimal) -## プロジェクト構造 +## Project Structure ``` techblog_cms/ -├── app/ # Djangoアプリケーション -│ ├── techblog_cms/ # メインDjangoアプリ +├── app/ # Django application +│ ├── techblog_cms/ # Main Django app │ │ ├── __init__.py -│ │ ├── settings.py # Django設定(環境変数使用) -│ │ ├── urls.py # URLマッピング -│ │ ├── views.py # ビュー関数 -│ │ ├── wsgi.py # WSGIエントリーポイント -│ │ └── templates/ # HTMLテンプレート -│ └── requirements.txt # Python依存関係 -├── nginx/ # Nginx設定 +│ │ ├── settings.py # Django settings (using environment variables) +│ │ ├── urls.py # URL mapping +│ │ ├── views.py # View functions +│ │ ├── wsgi.py # WSGI entry point +│ │ └── templates/ # HTML templates +│ └── requirements.txt # Python dependencies +├── nginx/ # Nginx configuration │ ├── conf.d/ -│ │ └── default.conf # Nginx設定ファイル -│ └── Dockerfile # Nginxコンテナ定義 -├── scripts/ # ユーティリティスクリプト -│ ├── init-letsencrypt.sh # SSL証明書初期化 -│ └── renew-cert.sh # SSL証明書更新 -├── static/ # 静的ファイル -├── tests/ # テストファイル -├── docker-compose.yml # コンテナオーケストレーション -├── Dockerfile.* # 各種Dockerfile -├── requirements.txt # プロジェクト全体の依存関係 -└── pytest.ini # pytest設定 +│ │ └── default.conf # Nginx configuration file +│ └── Dockerfile # Nginx container definition +├── scripts/ # Utility scripts +│ ├── init-letsencrypt.sh # Initialize SSL certificate +│ └── renew-cert.sh # Renew SSL certificate +├── static/ # Static files +├── tests/ # Test files +├── docker-compose.yml # Container orchestration +├── Dockerfile.* # Various Dockerfiles +├── requirements.txt # Project-wide dependencies +└── pytest.ini # pytest configuration ``` -## 開発ガイドライン - -### コーディング標準 -- **Python**: PEP 8準拠、Blackフォーマッター使用 -- **Django**: Djangoベストプラクティスに従う -- **Docker**: マルチステージビルド、セキュリティスキャン(Trivy) -- **セキュリティ**: 環境変数で機密情報を管理、HTTPS強制 - -### 環境変数 -重要な設定は環境変数で管理: -- `DEBUG`: デバッグモード(本番ではFalse) -- `SECRET_KEY`: Djangoシークレットキー -- `DATABASE_URL`: PostgreSQL接続文字列 -- `REDIS_URL`: Redis接続文字列 -- `ALLOWED_HOSTS`: 許可ホストリスト - -### テスト -- pytestを使用したユニットテストと統合テスト -- CI/CDで自動テスト実行 -- カバレッジレポート生成 - -## AIアシスタントへの指示 - -### コード生成時の考慮事項 -1. **セキュリティ優先**: 機密情報は環境変数を使用。ハードコーディング禁止。 -2. **コンテナ化対応**: Dockerベストプラクティスに従い、軽量イメージを作成。 -3. **Djangoベストプラクティス**: ビュー関数、モデル、テンプレートの適切な分離。 -4. **エラーハンドリング**: 適切な例外処理とログ出力。 -5. **パフォーマンス**: データベースクエリの最適化、キャッシュの活用。 - -### 支援時のガイドライン -1. **新規機能追加**: 既存の構造に準拠。必要に応じてマイグレーション作成。 -2. **バグ修正**: テストケースを追加し、リグレッションを防ぐ。 -3. **ドキュメント更新**: コード変更時はREADME.mdやこのAGENTS.mdを更新。 -4. **依存関係**: 新しいパッケージ追加時はrequirements.txtを更新。 -5. **Docker設定**: 変更時はdocker-compose.ymlと関連Dockerfileを確認。 - -### 禁止事項 -- ハードコーディングされたパスワードやAPIキー -- 非効率なデータベースクエリ -- セキュリティホール(SQLインジェクション、XSSなど) -- 不要な依存関係の追加 - -### 推奨ツール使用 -- **コード編集**: replace_string_in_file または insert_edit_into_file -- **ファイル作成**: create_file -- **ターミナル実行**: run_in_terminal(Dockerコマンドなど) -- **テスト実行**: runTests -- **ファイル検索**: grep_search または semantic_search - -## 貢献ガイド -1. developブランチからフィーチャーブランチを作成 -2. 変更を実装し、テストを追加 -3. プルリクエストを作成し、レビューを依頼 -4. マージ後、必要に応じてドキュメント更新 - -## 連絡先 -質問や改善提案は、GitHub IssuesまたはPull Requestsをご利用ください。 +## Development Guidelines + +### Coding Standards +- **Python**: PEP 8 compliant, using Black formatter +- **Django**: Follow Django best practices +- **Docker**: Multi-stage builds, security scanning (Trivy) +- **Security**: Manage sensitive info via environment variables, enforce HTTPS + +### Environment Variables +Critical settings managed via environment variables: +- `DEBUG`: Debug mode (False in production) +- `SECRET_KEY`: Django secret key +- `DATABASE_URL`: PostgreSQL connection string +- `REDIS_URL`: Redis connection string +- `ALLOWED_HOSTS`: List of allowed hosts + + +### Testing +- Unit and integration testing using pytest +- Automated test execution via CI/CD +- Coverage report generation + + +## Instructions for AI Assistant + + +### Considerations for Code Generation +1. **Security First**: Use environment variables for sensitive data. Do not hardcode. +2. **Containerization Ready**: Follow Docker best practices to create lightweight images. +3. **Django Best Practices**: Properly separate view functions, models, and templates. +4. **Error Handling**: Implement proper exception handling and log output. +5. **Performance**: Optimize database queries and utilize caching. + +### Guidelines for Support +1. **Adding New Features**: Adhere to existing structure. Create migrations as needed. +2. **Bug Fixes**: Add test cases to prevent regressions. +3. **Document Updates**: Update README.md and this AGENTS.md when modifying code. +4. **Dependencies**: Update requirements.txt when adding new packages. +5. **Docker Configuration**: Verify docker-compose.yml and related Dockerfiles when making changes. + +### Prohibited Actions +- Hard-coded passwords or API keys +- Inefficient database queries +- Security vulnerabilities (SQL injection, XSS, etc.) +- Adding unnecessary dependencies + +### Recommended Tool Usage +- **Code Editing**: `replace_string_in_file` or `insert_edit_into_file` +- **File Creation**: `create_file` +- **Terminal Execution**: run_in_terminal (e.g., Docker commands) +- **Test Execution**: runTests +- **File Search**: grep_search or semantic_search + +## Contribution Guide +1. Create a feature branch from the develop branch +2. If the current branch is main, create the feature branch from main. +2. Implement changes and add tests. +3. Create a pull request and request a review. +4. After merging, update documentation as needed. + +## Contact +Please use GitHub Issues or Pull Requests for questions or improvement suggestions. --- -最終更新: 2025年8月31日 +Last updated: August 31, 2025