diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7294025..0000000 --- a/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -# Python -__pycache__/ -*.py[cod] -*$py.class -*.so -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg - -# Django -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal -media/ -staticfiles/ - -# Virtual Environment -venv/ -ENV/ -env/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# Environment variables -.env - -# OS -.DS_Store -Thumbs.db \ No newline at end of file diff --git a/News_Assistant/settings.py b/News_Assistant/settings.py new file mode 100644 index 0000000..082d5e8 --- /dev/null +++ b/News_Assistant/settings.py @@ -0,0 +1,15 @@ +# Email Configuration +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_USE_SSL = False + +# Thay thế bằng Gmail của bạn +EMAIL_HOST_USER = 'phantonbasang@gmail.com' + +# Thay thế bằng mật khẩu ứng dụng từ Google Account +EMAIL_HOST_PASSWORD = 'swer dhtw xpbi bjjs' + +# Tên hiển thị trong email +DEFAULT_FROM_EMAIL = 'RAUCON_VN News ' \ No newline at end of file diff --git a/README.md b/README.md index 9e87d5d..7408e0c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,7 @@ -# Hướng dẫn cài đặt và chạy dự án web - -## Yêu cầu hệ thống -- Python 3.8 trở lên -- pip (Python package installer) -- Git (tùy chọn, để clone repository) ## Các bước cài đặt -### 1. Clone repository (nếu sử dụng Git) +### 1. Clone repository ```bash git clone cd @@ -51,30 +45,3 @@ python manage.py runserver ### 8. Truy cập website - Mở trình duyệt web và truy cập: `http://127.0.0.1:8000` -- Đăng nhập với tài khoản superuser đã tạo ở bước 6 - -## Cấu trúc thư mục -``` -project/ -├── config/ # Cấu hình chính của dự án -├── home/ # Ứng dụng chính -├── static/ # File tĩnh (CSS, JS, images) -├── templates/ # Templates HTML -├── manage.py # Script quản lý Django -└── requirements.txt # Danh sách các thư viện cần thiết -``` - -## Xử lý lỗi thường gặp -1. Nếu gặp lỗi "ModuleNotFoundError": - - Kiểm tra môi trường ảo đã được kích hoạt chưa - - Chạy lại lệnh `pip install -r requirements.txt` - -2. Nếu gặp lỗi database: - - Kiểm tra cấu hình database trong `settings.py` - - Chạy lại các lệnh migration - -3. Nếu gặp lỗi static files: - - Chạy lệnh `python manage.py collectstatic` - -## Liên hệ -Nếu bạn gặp bất kỳ vấn đề nào trong quá trình cài đặt, vui lòng liên hệ với quản trị viên hệ thống. \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..4ec376f Binary files /dev/null and b/db.sqlite3 differ diff --git a/home/__pycache__/__init__.cpython-310.pyc b/home/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f89e736 Binary files /dev/null and b/home/__pycache__/__init__.cpython-310.pyc differ diff --git a/home/__pycache__/admin.cpython-310.pyc b/home/__pycache__/admin.cpython-310.pyc new file mode 100644 index 0000000..5134bc1 Binary files /dev/null and b/home/__pycache__/admin.cpython-310.pyc differ diff --git a/home/__pycache__/apps.cpython-310.pyc b/home/__pycache__/apps.cpython-310.pyc new file mode 100644 index 0000000..a6e678b Binary files /dev/null and b/home/__pycache__/apps.cpython-310.pyc differ diff --git a/home/__pycache__/custom_field.cpython-310.pyc b/home/__pycache__/custom_field.cpython-310.pyc new file mode 100644 index 0000000..ce656c1 Binary files /dev/null and b/home/__pycache__/custom_field.cpython-310.pyc differ diff --git a/home/__pycache__/define.cpython-310.pyc b/home/__pycache__/define.cpython-310.pyc new file mode 100644 index 0000000..112d214 Binary files /dev/null and b/home/__pycache__/define.cpython-310.pyc differ diff --git a/home/__pycache__/helpers.cpython-310.pyc b/home/__pycache__/helpers.cpython-310.pyc new file mode 100644 index 0000000..d9f053a Binary files /dev/null and b/home/__pycache__/helpers.cpython-310.pyc differ diff --git a/home/__pycache__/models.cpython-310.pyc b/home/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..fecf7d4 Binary files /dev/null and b/home/__pycache__/models.cpython-310.pyc differ diff --git a/home/__pycache__/rss_classifier.cpython-310.pyc b/home/__pycache__/rss_classifier.cpython-310.pyc new file mode 100644 index 0000000..7d6e6b7 Binary files /dev/null and b/home/__pycache__/rss_classifier.cpython-310.pyc differ diff --git a/home/__pycache__/signals.cpython-310.pyc b/home/__pycache__/signals.cpython-310.pyc new file mode 100644 index 0000000..f4d47f4 Binary files /dev/null and b/home/__pycache__/signals.cpython-310.pyc differ diff --git a/home/__pycache__/urls.cpython-310.pyc b/home/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000..61ccb44 Binary files /dev/null and b/home/__pycache__/urls.cpython-310.pyc differ diff --git a/home/__pycache__/views.cpython-310.pyc b/home/__pycache__/views.cpython-310.pyc new file mode 100644 index 0000000..6b43097 Binary files /dev/null and b/home/__pycache__/views.cpython-310.pyc differ diff --git a/home/admin.py b/home/admin.py index e69de29..bcc3831 100644 --- a/home/admin.py +++ b/home/admin.py @@ -0,0 +1,116 @@ +from django.contrib import admin +from django.core.mail import send_mail +from django.utils import timezone +from django.conf import settings +from .define import * + +from .models import Category, Article, Feed, Author, CategoryFeed, Contact + +class CategoryAdmin (admin.ModelAdmin): + list_display = ('name', 'status', 'is_homepage', 'layout', 'ordering') + #prepopulated_fields = {'slug':('name',)} #tạo ra mối quan hệ giữa 2 cái này để nó chạy tự động + list_filter = ["is_homepage","status","layout"] + search_fields = ['name'] + + class Media: + js = ADMIN_SRC_JS + css = ADMIN_SRC_CSS + +class ArticleAdmin (admin.ModelAdmin): + list_display = ('name', 'author', 'category', 'status', 'ordering') + #prepopulated_fields = {'slug':('name',)} #tạo ra mối quan hệ giữa 2 cái này để nó chạy tự động + list_filter = ["status", "special", "category", "author"] + search_fields = ['name', 'content'] + + class Media: + js = ADMIN_SRC_JS + css = ADMIN_SRC_CSS + +class FeedAdmin (admin.ModelAdmin): + list_display = ('name', 'status', 'ordering') + #prepopulated_fields = {'slug':('name',)} #tạo ra mối quan hệ giữa 2 cái này để nó chạy tự động + list_filter = ["status"] + search_fields = ['name'] + + class Media: + js = ADMIN_SRC_JS + +# Thêm AuthorAdmin mới +class AuthorAdmin(admin.ModelAdmin): + list_display = ('name', 'email', 'status') + list_filter = ["status"] + search_fields = ['name', 'email'] + + class Media: + js = ADMIN_SRC_JS + css = ADMIN_SRC_CSS +# thêm CategoryFeed cho RSS_Danh_Muc +class CategoryFeedAdmin(admin.ModelAdmin): + list_display = ('name', 'category', 'status', 'ordering') + list_filter = ('status', 'category') + search_fields = ('name',) + +@admin.register(Contact) +class ContactAdmin(admin.ModelAdmin): + list_display = ('name', 'email', 'subject', 'created_at', 'status') + list_filter = ('status', 'created_at') + search_fields = ('name', 'email', 'subject', 'message') + readonly_fields = ('created_at',) + fieldsets = ( + ('Thông tin người gửi', { + 'fields': ('name', 'email', 'created_at') + }), + ('Nội dung tin nhắn', { + 'fields': ('subject', 'message') + }), + ('Phản hồi', { + 'fields': ('status', 'reply', 'replied_at') + }), + ) + + def save_model(self, request, obj, form, change): + if change and 'reply' in form.changed_data: # Nếu phản hồi được thay đổi + obj.status = 'replied' + obj.replied_at = timezone.now() + + # Gửi email phản hồi + email_subject = f'[RAUCON_VN] Phản hồi cho tin nhắn: {obj.subject}' + email_message = f""" +Xin chào {obj.name}, + +Cảm ơn bạn đã liên hệ với RAUCON_VN. Dưới đây là phản hồi cho tin nhắn của bạn: + +Tin nhắn của bạn: +{obj.message} + +Phản hồi của chúng tôi: +{obj.reply} + +Trân trọng, +Đội ngũ RAUCON_VN + """ + + try: + send_mail( + subject=email_subject, + message=email_message, + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[obj.email], + fail_silently=False, + ) + except Exception as e: + self.message_user(request, f"Không thể gửi email: {str(e)}", level='ERROR') + else: + self.message_user(request, "Phản hồi đã được gửi qua email thành công!", level='SUCCESS') + + super().save_model(request, obj, form, change) + +admin.site.register(Category, CategoryAdmin) +admin.site.register(Article, ArticleAdmin) +admin.site.register(Feed, FeedAdmin) +admin.site.register(Author, AuthorAdmin) +admin.site.register(CategoryFeed, CategoryFeedAdmin) +# chỉnh tên trang admin thay vì để mặt định là Django +admin.site.site_header = ADMIN_HEADER_NAME +admin.site.site_title = ADMIN_HEADER_NAME +admin.site.index_title = 'Dashboard' \ No newline at end of file diff --git a/home/custom_field.py b/home/custom_field.py new file mode 100644 index 0000000..b209b20 --- /dev/null +++ b/home/custom_field.py @@ -0,0 +1,9 @@ +from django.forms.widgets import RadioSelect +from django.db import models + +class CustomBooleanField (models.BooleanField): + def formfield(self, **kwargs): + kwargs['widget'] = RadioSelect (choices= ((True,'Có'), (False, 'Không'))) + kwargs['initial'] = False + + return super().formfield(**kwargs) \ No newline at end of file diff --git a/home/define.py b/home/define.py new file mode 100644 index 0000000..6d56477 --- /dev/null +++ b/home/define.py @@ -0,0 +1,22 @@ +# models.py +APP_VALUE_LAYOUT_DEFAULT = "list" +APP_VALUE_STATUS_DEFAULT = 'draft' + +APP_VALUE_STATUS_CHOICES = ( + ('draft', 'Draft'), + ('published', 'Published') + ) + +APP_VALUE_LAYOUT_CHOICES = ( + ('list', 'List'), + ('grid', 'Grid') + ) + +TABLE_CATEGORY_SHOW = "Category" +TABLE_ARTICLE_SHOW = "Article" +TABLE_FEED_SHOW = "Feed" + +# admin.py +ADMIN_SRC_JS = ('my_admin/js/jquery-3.6.0.min.js','my_admin/js/slugify.min.js','my_admin/js/general.js') +ADMIN_SRC_CSS = {'all' : ('my_admin/css/custom.css',)} +ADMIN_HEADER_NAME = "news administration " \ No newline at end of file diff --git a/home/helpers.py b/home/helpers.py new file mode 100644 index 0000000..13bbe04 --- /dev/null +++ b/home/helpers.py @@ -0,0 +1,7 @@ +import os +import uuid + +def get_file_path (instance, filename): + ext = filename.split ('.')[-1] + filename = '%s.%s' % (uuid.uuid4(),ext) + return os.path.join ('news_app/image/article/',filename) \ No newline at end of file diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py new file mode 100644 index 0000000..89dcd6c --- /dev/null +++ b/home/migrations/0001_initial.py @@ -0,0 +1,64 @@ +# Generated by Django 5.0.2 on 2025-03-30 00:38 + +import django.db.models.deletion +import home.custom_field +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Category', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.CharField(max_length=100, unique=True)), + ('is_homepage', home.custom_field.CustomBooleanField()), + ('layout', models.CharField(choices=[('list', 'List'), ('grid', 'Grid')], default='list', max_length=10)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), + ('ordering', models.IntegerField(default=0)), + ], + options={ + 'verbose_name_plural': 'Category', + }, + ), + migrations.CreateModel( + name='Feed', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.CharField(max_length=100, unique=True)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), + ('ordering', models.IntegerField(default=0)), + ('link', models.CharField(max_length=250)), + ], + options={ + 'verbose_name_plural': 'Feed', + }, + ), + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.CharField(max_length=100, unique=True)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), + ('ordering', models.IntegerField(default=0)), + ('special', home.custom_field.CustomBooleanField()), + ('publish_date', models.DateTimeField()), + ('content', tinymce.models.HTMLField()), + ('image', models.ImageField(upload_to='news_app/image/article/')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='home.category')), + ], + options={ + 'verbose_name_plural': 'Article', + }, + ), + ] diff --git a/home/migrations/0002_alter_article_options_alter_category_options_and_more.py b/home/migrations/0002_alter_article_options_alter_category_options_and_more.py new file mode 100644 index 0000000..f2a6bee --- /dev/null +++ b/home/migrations/0002_alter_article_options_alter_category_options_and_more.py @@ -0,0 +1,111 @@ +# Generated by Django 5.0.2 on 2025-03-30 12:40 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={}, + ), + migrations.AlterModelOptions( + name='category', + options={}, + ), + migrations.AlterModelOptions( + name='feed', + options={'verbose_name_plural': 'Feeds'}, + ), + migrations.AddField( + model_name='feed', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='home.category'), + ), + migrations.AlterField( + model_name='article', + name='content', + field=models.TextField(), + ), + migrations.AlterField( + model_name='article', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='articles/'), + ), + migrations.AlterField( + model_name='article', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='article', + name='publish_date', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='article', + name='slug', + field=models.SlugField(blank=True, unique=True), + ), + migrations.AlterField( + model_name='article', + name='special', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='article', + name='status', + field=models.CharField(choices=[('published', 'Xuất bản'), ('draft', 'Nháp')], default='published', max_length=20), + ), + migrations.AlterField( + model_name='category', + name='is_homepage', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='category', + name='layout', + field=models.CharField(choices=[('grid', 'Lưới'), ('list', 'Danh sách')], default='grid', max_length=20), + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='category', + name='slug', + field=models.SlugField(blank=True, unique=True), + ), + migrations.AlterField( + model_name='category', + name='status', + field=models.CharField(choices=[('published', 'Xuất bản'), ('draft', 'Nháp')], default='published', max_length=20), + ), + migrations.AlterField( + model_name='feed', + name='link', + field=models.URLField(), + ), + migrations.AlterField( + model_name='feed', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='feed', + name='slug', + field=models.SlugField(blank=True, unique=True), + ), + migrations.AlterField( + model_name='feed', + name='status', + field=models.CharField(choices=[('published', 'Xuất bản'), ('draft', 'Nháp')], default='published', max_length=20), + ), + ] diff --git a/home/migrations/0003_alter_article_options_alter_category_options_and_more.py b/home/migrations/0003_alter_article_options_alter_category_options_and_more.py new file mode 100644 index 0000000..1a8058d --- /dev/null +++ b/home/migrations/0003_alter_article_options_alter_category_options_and_more.py @@ -0,0 +1,141 @@ +# Generated by Django 5.0.2 on 2025-04-02 08:50 + +import django.db.models.deletion +import home.custom_field +import tinymce.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0002_alter_article_options_alter_category_options_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'verbose_name_plural': 'Article'}, + ), + migrations.AlterModelOptions( + name='category', + options={'verbose_name_plural': 'Category'}, + ), + migrations.AlterModelOptions( + name='feed', + options={'verbose_name_plural': 'Feed'}, + ), + migrations.RemoveField( + model_name='feed', + name='category', + ), + migrations.AlterField( + model_name='article', + name='category', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to='home.category'), + ), + migrations.AlterField( + model_name='article', + name='content', + field=tinymce.models.HTMLField(), + ), + migrations.AlterField( + model_name='article', + name='image', + field=models.ImageField(default='default.jpg', upload_to='images/'), + ), + migrations.AlterField( + model_name='article', + name='name', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='article', + name='publish_date', + field=models.DateTimeField(), + ), + migrations.AlterField( + model_name='article', + name='slug', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='article', + name='special', + field=home.custom_field.CustomBooleanField(), + ), + migrations.AlterField( + model_name='article', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10), + ), + migrations.AlterField( + model_name='category', + name='is_homepage', + field=home.custom_field.CustomBooleanField(), + ), + migrations.AlterField( + model_name='category', + name='layout', + field=models.CharField(choices=[('list', 'List'), ('grid', 'Grid')], default='list', max_length=10), + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='category', + name='slug', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='category', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10), + ), + migrations.AlterField( + model_name='feed', + name='link', + field=models.CharField(max_length=250), + ), + migrations.AlterField( + model_name='feed', + name='name', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='feed', + name='slug', + field=models.CharField(max_length=100, unique=True), + ), + migrations.AlterField( + model_name='feed', + name='status', + field=models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10), + ), + migrations.CreateModel( + name='Author', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('bio', tinymce.models.HTMLField(blank=True, null=True)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='news_app/image/author/')), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('facebook', models.URLField(blank=True, null=True)), + ('twitter', models.URLField(blank=True, null=True)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name_plural': 'Authors', + }, + ), + migrations.AddField( + model_name='article', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='home.author'), + ), + ] diff --git a/home/migrations/0004_alter_feed_options_remove_author_twitter_and_more.py b/home/migrations/0004_alter_feed_options_remove_author_twitter_and_more.py new file mode 100644 index 0000000..0e02959 --- /dev/null +++ b/home/migrations/0004_alter_feed_options_remove_author_twitter_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.2 on 2025-04-05 11:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0003_alter_article_options_alter_category_options_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='feed', + options={}, + ), + migrations.RemoveField( + model_name='author', + name='twitter', + ), + migrations.AddField( + model_name='category', + name='link', + field=models.CharField(default='https://example.com', max_length=250), + ), + ] diff --git a/home/migrations/0005_alter_category_link.py b/home/migrations/0005_alter_category_link.py new file mode 100644 index 0000000..c96c302 --- /dev/null +++ b/home/migrations/0005_alter_category_link.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.2 on 2025-04-05 11:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0004_alter_feed_options_remove_author_twitter_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='link', + field=models.CharField(max_length=250), + ), + ] diff --git a/home/migrations/0006_categoryfeed.py b/home/migrations/0006_categoryfeed.py new file mode 100644 index 0000000..4978590 --- /dev/null +++ b/home/migrations/0006_categoryfeed.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.2 on 2025-04-05 12:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0005_alter_category_link'), + ] + + operations = [ + migrations.CreateModel( + name='CategoryFeed', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('slug', models.CharField(max_length=100)), + ('rss_url', models.URLField()), + ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), + ('ordering', models.IntegerField(default=0)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='category_feeds', to='home.category')), + ], + options={ + 'verbose_name_plural': 'Category Feeds', + }, + ), + ] diff --git a/home/migrations/0007_alter_category_link_alter_categoryfeed_slug.py b/home/migrations/0007_alter_category_link_alter_categoryfeed_slug.py new file mode 100644 index 0000000..5370991 --- /dev/null +++ b/home/migrations/0007_alter_category_link_alter_categoryfeed_slug.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.2 on 2025-04-07 02:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0006_categoryfeed'), + ] + + operations = [ + migrations.AlterField( + model_name='category', + name='link', + field=models.CharField(blank=True, max_length=250, null=True), + ), + migrations.AlterField( + model_name='categoryfeed', + name='slug', + field=models.CharField(max_length=100, unique=True), + ), + ] diff --git a/home/migrations/0008_remove_category_link.py b/home/migrations/0008_remove_category_link.py new file mode 100644 index 0000000..9e30052 --- /dev/null +++ b/home/migrations/0008_remove_category_link.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.2 on 2025-04-07 02:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0007_alter_category_link_alter_categoryfeed_slug'), + ] + + operations = [ + migrations.RemoveField( + model_name='category', + name='link', + ), + ] diff --git a/home/migrations/0009_contact.py b/home/migrations/0009_contact.py new file mode 100644 index 0000000..9f1f995 --- /dev/null +++ b/home/migrations/0009_contact.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.2 on 2025-04-07 02:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0008_remove_category_link'), + ] + + operations = [ + migrations.CreateModel( + name='Contact', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('email', models.EmailField(max_length=254)), + ('subject', models.CharField(max_length=200)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('status', models.CharField(choices=[('pending', 'Đang chờ'), ('replied', 'Đã phản hồi')], default='pending', max_length=20)), + ('reply', models.TextField(blank=True, null=True)), + ('replied_at', models.DateTimeField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Liên hệ', + 'verbose_name_plural': 'Liên hệ', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/home/apps.py b/home/migrations/__init__.py similarity index 100% rename from home/apps.py rename to home/migrations/__init__.py diff --git a/home/migrations/__pycache__/0001_initial.cpython-310.pyc b/home/migrations/__pycache__/0001_initial.cpython-310.pyc new file mode 100644 index 0000000..5a89d06 Binary files /dev/null and b/home/migrations/__pycache__/0001_initial.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0002_alter_article_options_alter_category_options_and_more.cpython-310.pyc b/home/migrations/__pycache__/0002_alter_article_options_alter_category_options_and_more.cpython-310.pyc new file mode 100644 index 0000000..98b7a6f Binary files /dev/null and b/home/migrations/__pycache__/0002_alter_article_options_alter_category_options_and_more.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0003_alter_article_options_alter_category_options_and_more.cpython-310.pyc b/home/migrations/__pycache__/0003_alter_article_options_alter_category_options_and_more.cpython-310.pyc new file mode 100644 index 0000000..32eb678 Binary files /dev/null and b/home/migrations/__pycache__/0003_alter_article_options_alter_category_options_and_more.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0004_alter_feed_options_remove_author_twitter_and_more.cpython-310.pyc b/home/migrations/__pycache__/0004_alter_feed_options_remove_author_twitter_and_more.cpython-310.pyc new file mode 100644 index 0000000..782cb91 Binary files /dev/null and b/home/migrations/__pycache__/0004_alter_feed_options_remove_author_twitter_and_more.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0005_alter_category_link.cpython-310.pyc b/home/migrations/__pycache__/0005_alter_category_link.cpython-310.pyc new file mode 100644 index 0000000..fa34827 Binary files /dev/null and b/home/migrations/__pycache__/0005_alter_category_link.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0006_categoryfeed.cpython-310.pyc b/home/migrations/__pycache__/0006_categoryfeed.cpython-310.pyc new file mode 100644 index 0000000..befc932 Binary files /dev/null and b/home/migrations/__pycache__/0006_categoryfeed.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0007_alter_category_link_alter_categoryfeed_slug.cpython-310.pyc b/home/migrations/__pycache__/0007_alter_category_link_alter_categoryfeed_slug.cpython-310.pyc new file mode 100644 index 0000000..a82ac28 Binary files /dev/null and b/home/migrations/__pycache__/0007_alter_category_link_alter_categoryfeed_slug.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0008_remove_category_link.cpython-310.pyc b/home/migrations/__pycache__/0008_remove_category_link.cpython-310.pyc new file mode 100644 index 0000000..4880650 Binary files /dev/null and b/home/migrations/__pycache__/0008_remove_category_link.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/0009_contact.cpython-310.pyc b/home/migrations/__pycache__/0009_contact.cpython-310.pyc new file mode 100644 index 0000000..e2fb8fc Binary files /dev/null and b/home/migrations/__pycache__/0009_contact.cpython-310.pyc differ diff --git a/home/migrations/__pycache__/__init__.cpython-310.pyc b/home/migrations/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..5b00403 Binary files /dev/null and b/home/migrations/__pycache__/__init__.cpython-310.pyc differ diff --git a/home/models.py b/home/models.py index e69de29..8a5719e 100644 --- a/home/models.py +++ b/home/models.py @@ -0,0 +1,127 @@ +from django.db import models +from tinymce.models import HTMLField + +from .helpers import * +from .custom_field import * +from .define import * +from django.contrib.auth.models import User + +# Model Author mới +class Author(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True) + name = models.CharField(max_length=100) + bio = HTMLField(blank=True, null=True) + avatar = models.ImageField(upload_to='news_app/image/author/', blank=True, null=True) + email = models.EmailField(blank=True, null=True) + facebook = models.URLField(blank=True, null=True) + status = models.CharField(max_length=10, choices=APP_VALUE_STATUS_CHOICES, default=APP_VALUE_STATUS_DEFAULT) + + class Meta: + verbose_name_plural = "Authors" + + def __str__(self): + return self.name + +class Category (models.Model): + + # Tạo các trường + name = models.CharField(unique=True, max_length=100) + slug = models.CharField(unique=True, max_length=100) + is_homepage = CustomBooleanField() + layout = models.CharField(max_length=10, choices=APP_VALUE_LAYOUT_CHOICES, default=APP_VALUE_LAYOUT_DEFAULT) #define.py + status = models.CharField(max_length=10, choices=APP_VALUE_STATUS_CHOICES, default=APP_VALUE_STATUS_DEFAULT) # nháp #define.py + ordering = models.IntegerField(default=0) + + + # chỉnh tên của các models + class Meta: + verbose_name_plural = TABLE_CATEGORY_SHOW #define.py + + def __str__(self): + return self.name + +# RSS_DANH_MUC models.py +class CategoryFeed(models.Model): + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='category_feeds') + name = models.CharField(max_length=100) + slug = models.CharField (unique=True, max_length= 100) + rss_url = models.URLField() + status = models.CharField(max_length=10, choices=APP_VALUE_STATUS_CHOICES, default=APP_VALUE_STATUS_DEFAULT) + ordering = models.IntegerField(default=0) + + class Meta: + verbose_name_plural = "Category Feeds" + + def __str__(self): + return f"{self.category.name} - {self.name}" + + +class Article(models.Model): + # Các trường hiện có + name = models.CharField(unique=True, max_length=100) + slug = models.CharField(unique=True, max_length=100) + status = models.CharField(max_length=10, choices=APP_VALUE_STATUS_CHOICES, default=APP_VALUE_STATUS_DEFAULT) + ordering = models.IntegerField(default=0) + special = CustomBooleanField() + publish_date = models.DateTimeField() + content = HTMLField() + image = models.ImageField(upload_to='images/', null=False, default='default.jpg') + category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='articles') + + # Thêm trường author + author = models.ForeignKey(Author, on_delete=models.SET_NULL, null=True, blank=True, related_name='articles') + + class Meta: + verbose_name_plural = TABLE_ARTICLE_SHOW + + def __str__(self): + return self.name + + +class Feed (models.Model): + + # Tạo các trường + name = models.CharField (unique=True, max_length= 100) + slug = models.CharField (unique=True, max_length= 100) + status = models.CharField (max_length=10, choices=APP_VALUE_STATUS_CHOICES, default=APP_VALUE_STATUS_DEFAULT) # nháp + ordering =models.IntegerField (default=0) + link = models.CharField (max_length=250) + + + +class Meta: + verbose_name_plural = TABLE_FEED_SHOW #define.py + +def __str__(self): + return self.name + + +class Contact(models.Model): + STATUS_CHOICES = [ + ('pending', 'Đang chờ'), + ('replied', 'Đã phản hồi'), + ] + + name = models.CharField(max_length=100) + email = models.EmailField() + subject = models.CharField(max_length=200) + message = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + reply = models.TextField(blank=True, null=True) + replied_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['-created_at'] + verbose_name = 'Liên hệ' + verbose_name_plural = 'Liên hệ' + + def __str__(self): + return f"{self.name} - {self.subject}" + +class Meta: + verbose_name_plural = TABLE_FEED_SHOW #define.py + +def __str__(self): + return self.name + diff --git a/home/urls.py b/home/urls.py index 7710180..bd496ce 100644 --- a/home/urls.py +++ b/home/urls.py @@ -1,3 +1,5 @@ +# urls.py + from django.urls import path from . import views @@ -6,4 +8,13 @@ path('login/', views.login_view, name='login'), path('register/', views.register_view, name='register'), path('logout/', views.logout_view, name='logout'), + path('category//', views.category_view, name='category_detail'), # URL cho trang danh mục + # Thêm URL pattern mới cho Ajax Load More + path('load-more-news/', views.load_more_news, name='load_more_news'), + # URL pattern mới cho trang tác giả + path('author//', views.author_view, name='author_detail'), + path('about/', views.about_view, name='about'), + path('contact/', views.contact_view, name='contact'), + path('check-reply-status//', views.check_reply_status, name='check_reply_status'), + path('test-email/', views.test_email, name='test_email'), ] diff --git a/home/views.py b/home/views.py index 3c39f2f..86497f4 100644 --- a/home/views.py +++ b/home/views.py @@ -1,19 +1,157 @@ -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import login as auth_login, logout as auth_logout, authenticate from django.contrib.auth.models import User from django.contrib import messages +from django.contrib.auth.decorators import user_passes_test + +#news rss +from django.shortcuts import render, redirect +import feedparser +from bs4 import BeautifulSoup +from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout +from django.contrib.auth.models import User +from django.contrib import messages +#news rss + +from .models import Category, Article, Feed, Author, CategoryFeed, Contact # Import model Category, Article và Feed + +# ajax load more news +from django.http import JsonResponse +from django.template.loader import render_to_string +from django.views.decorators.http import require_http_methods +from django.utils import timezone +import json + +from django.core.mail import send_mail +from django.http import HttpResponse + +def is_admin(user): + return user.is_staff def home_view(request): - return render(request, 'home/home.html') + selected_feed_slug = request.GET.get('feed', None) + selected_category_slug = request.GET.get('category', None) + default_rss_feed_url = 'https://vnexpress.net/rss/tin-moi-nhat.rss' + rss_feed_url = default_rss_feed_url + source_name = "VNExpress" # Default source name + + # Luôn lấy tất cả categories đã publish + categories = Category.objects.filter(status='published').order_by('ordering') + feeds = Feed.objects.filter(status='published').order_by('ordering') if request.user.is_authenticated else None + + if selected_feed_slug: + selected_feed = get_object_or_404(Feed, slug=selected_feed_slug, status='published') + rss_feed_url = selected_feed.link + source_name = selected_feed.name + elif selected_category_slug: + # Get the category and its feeds + category = get_object_or_404(Category, slug=selected_category_slug, status='published') + category_feeds = CategoryFeed.objects.filter(category=category, status='published') + + iteam_rss = [] + # Get items from all category feeds + for cat_feed in category_feeds: + feed = feedparser.parse(cat_feed.rss_url) + + for item in feed.entries[:5]: # Limit to 5 items per feed + title = item.get('title') + pub_date = item.get('published') + link = item.get('link') + + description = item.get('summary', '') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + feed_item = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text, + 'image': img_src, + 'source': cat_feed.name + } + iteam_rss.append(feed_item) + + articles = Article.objects.filter(status='published').order_by('-publish_date')[:5] + + context = { + 'iteam_rss': iteam_rss, + 'categories': categories, + 'feeds': feeds, + 'articles': articles, + 'selected_category_slug': selected_category_slug, + } + return render(request, 'home/home.html', context) + + # Original code for default and feed-specific views + feed = feedparser.parse(rss_feed_url) + iteam_rss = [] + for i in feed.entries: + title = i.get('title') + pub_date = i.get('published') + link = i.get('link') + + description = i.get('summary') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + iteam = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text, + 'image': img_src, + 'source': source_name + } + iteam_rss.append(iteam) + + articles = Article.objects.filter(status='published').order_by('-publish_date')[:5] + + context = { + 'iteam_rss': iteam_rss, + 'categories': categories, + 'feeds': feeds, + 'articles': articles, + } + return render(request, 'home/home.html', context) + + +def category_view(request, category_slug): + category = get_object_or_404(Category, slug=category_slug, status='published') + articles = Article.objects.filter(category=category, status='published').order_by('-publish_date') + + context = { + 'category': category, + 'articles': articles, + } + return render(request, 'home/category_detail.html', context) + def login_view(request): if request.method == 'POST': username = request.POST['username'] password = request.POST['password'] + admin_login = request.POST.get('admin_login', False) + user = authenticate(username=username, password=password) if user: + if admin_login and not user.is_staff: + messages.error(request, "This account is not an administrator account!") + return render(request, 'home/login.html') auth_login(request, user) messages.success(request, "Successfully logged in!") + if admin_login and user.is_staff: + return redirect('/admin/') return redirect('home') messages.error(request, "Invalid credentials!") return render(request, 'home/login.html') @@ -44,3 +182,251 @@ def logout_view(request): auth_logout(request) messages.success(request, "Successfully logged out!") return redirect('home') + +# ajax load more news +def load_more_news(request): + page = int(request.GET.get('page', 1)) + selected_feed_slug = request.GET.get('feed', None) + selected_category_slug = request.GET.get('category', None) + items_per_page = 6 # Số lượng tin tức mỗi trang + + # Tính vị trí bắt đầu và kết thúc + start = (page - 1) * items_per_page + end = page * items_per_page + + item_rss = [] + + if selected_category_slug: + # Load more for category RSS feeds + category = get_object_or_404(Category, slug=selected_category_slug, status='published') + category_feeds = CategoryFeed.objects.filter(category=category, status='published') + + all_items = [] + for cat_feed in category_feeds: + feed = feedparser.parse(cat_feed.rss_url) + + for item in feed.entries: + title = item.get('title') + pub_date = item.get('published') + link = item.get('link') + + description = item.get('summary', '') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + feed_item = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text, + 'image': img_src, + 'source': cat_feed.name + } + all_items.append(feed_item) + + # Get items for current page + item_rss = all_items[start:end] + has_more = len(all_items) > end + + elif selected_feed_slug: + # Load more for specific feed + selected_feed = get_object_or_404(Feed, slug=selected_feed_slug, status='published') + feed = feedparser.parse(selected_feed.link) + all_items = feed.entries + + # Get items for current page + page_items = all_items[start:end] + + for i in page_items: + title = i.get('title') + pub_date = i.get('published') + link = i.get('link') + + description = i.get('summary') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + item = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text, + 'image': img_src, + 'source': selected_feed.name + } + item_rss.append(item) + + has_more = len(all_items) > end + + else: + # Load more for default feed (VnExpress) + default_rss_feed_url = 'https://vnexpress.net/rss/tin-moi-nhat.rss' + feed = feedparser.parse(default_rss_feed_url) + all_items = feed.entries + + # Get items for current page + page_items = all_items[start:end] + + for i in page_items: + title = i.get('title') + pub_date = i.get('published') + link = i.get('link') + + description = i.get('summary') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + item = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text, + 'image': img_src, + 'source': 'VNExpress' + } + item_rss.append(item) + + has_more = len(all_items) > end + + # Tạo HTML từ các items + html = render_to_string('home/news_items.html', {'iteam_rss': item_rss}) + + return JsonResponse({ + 'html': html, + 'has_more': has_more + }) + + +def author_view(request, author_slug): + # Lấy thông tin tác giả + author = get_object_or_404(Author, slug=author_slug, status='published') + + # Lấy các bài viết của tác giả + articles = Article.objects.filter(author=author, status='published').order_by('-publish_date') + + context = { + 'author': author, + 'articles': articles, + } + + return render(request, 'home/author_detail.html', context) + +def category_detail(request, slug): + category = get_object_or_404(Category, slug=slug) + articles = Article.objects.filter(category=category, status='published').order_by('-publish_date') + + # Get RSS feeds for this category + category_feeds = CategoryFeed.objects.filter(category=category, status='published') + feed_items = [] + + # Process each RSS feed + for cat_feed in category_feeds: + feed = feedparser.parse(cat_feed.rss_url) + + for item in feed.entries[:5]: # Limit to 5 items per feed + title = item.get('title') + pub_date = item.get('published') + link = item.get('link') + + # Extract description and image + description = item.get('summary', '') + description_soup = BeautifulSoup(description, 'html.parser') + description_text = description_soup.get_text() + + img_tag = description_soup.find('img') + img_src = "https://yt3.googleusercontent.com/BxWJ7TciIXwmJXFOKIpG9z-9WhjkYzuEw7_DX6gMsRvIEcYnxgPIfXQLzdZ4PkJMSt-YuIVRMcI=s900-c-k-c0x00ffffff-no-rj" + if img_tag: + img_src = img_tag['src'] + + feed_item = { + 'title': title, + 'pub_date': pub_date, + 'link': link, + 'description': description_text[:100] + '...' if len(description_text) > 100 else description_text, + 'image': img_src, + 'source': cat_feed.name + } + feed_items.append(feed_item) + + context = { + 'category': category, + 'articles': articles, + 'category_feeds': category_feeds, + 'feed_items': feed_items + } + + return render(request, 'home/category_detail.html', context) + +def about_view(request): + return render(request, 'home/about.html') + +@require_http_methods(["GET", "POST"]) +def contact_view(request): + if request.method == "POST": + if request.headers.get('x-requested-with') == 'XMLHttpRequest': + try: + data = json.loads(request.body) + contact = Contact.objects.create( + name=data.get('name'), + email=data.get('email'), + subject=data.get('subject'), + message=data.get('message') + ) + return JsonResponse({ + 'status': 'success', + 'message': 'Cảm ơn bạn đã liên hệ. Chúng tôi sẽ phản hồi sớm nhất có thể.', + 'contact_id': contact.id + }) + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': 'Có lỗi xảy ra. Vui lòng thử lại sau.' + }) + return render(request, 'home/contact.html') + +@require_http_methods(["GET"]) +def check_reply_status(request, contact_id): + try: + contact = Contact.objects.get(id=contact_id) + data = { + 'status': contact.status, + 'has_reply': bool(contact.reply), + 'reply': contact.reply if contact.reply else None, + 'replied_at': contact.replied_at.strftime("%d/%m/%Y %H:%M") if contact.replied_at else None + } + return JsonResponse(data) + except Contact.DoesNotExist: + return JsonResponse({ + 'status': 'error', + 'message': 'Không tìm thấy tin nhắn' + }) + +def test_email(request): + try: + result = send_mail( + 'Test Email', + 'This is a test email from Django', + 'RAUCON_VN News ', + ['phantonbasang@gmail.com'], + fail_silently=False, + ) + return HttpResponse(f"Email sent successfully! Result: {result}") + except Exception as e: + import traceback + error_details = traceback.format_exc() + return HttpResponse(f"Failed to send email. Error: {str(e)}\n\nDetails:\n{error_details}") \ No newline at end of file diff --git a/manage.py b/manage.py index 96e0375..a685a14 100644 --- a/manage.py +++ b/manage.py @@ -1,11 +1,9 @@ #!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" import os import sys def main(): - """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') try: from django.core.management import execute_from_command_line diff --git a/myproject/__pycache__/__init__.cpython-310.pyc b/myproject/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f82f36f Binary files /dev/null and b/myproject/__pycache__/__init__.cpython-310.pyc differ diff --git a/myproject/__pycache__/settings.cpython-310.pyc b/myproject/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000..fc480e6 Binary files /dev/null and b/myproject/__pycache__/settings.cpython-310.pyc differ diff --git a/myproject/__pycache__/urls.cpython-310.pyc b/myproject/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000..425aec8 Binary files /dev/null and b/myproject/__pycache__/urls.cpython-310.pyc differ diff --git a/myproject/__pycache__/wsgi.cpython-310.pyc b/myproject/__pycache__/wsgi.cpython-310.pyc new file mode 100644 index 0000000..726ab69 Binary files /dev/null and b/myproject/__pycache__/wsgi.cpython-310.pyc differ diff --git a/myproject/settings.py b/myproject/settings.py index 83f6ba7..8d83b14 100644 --- a/myproject/settings.py +++ b/myproject/settings.py @@ -21,6 +21,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'home', + 'tinymce', ] MIDDLEWARE = [ diff --git a/news_app/image/author/3_avatar.png b/news_app/image/author/3_avatar.png new file mode 100644 index 0000000..e738f32 Binary files /dev/null and b/news_app/image/author/3_avatar.png differ diff --git a/requirements.txt b/requirements.txt index 3a250b3..ba7f761 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,12 @@ python-dotenv==1.0.1 django-allauth==0.61.1 django-cleanup==8.1.0 whitenoise==6.6.0 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +# newss +feedparser==6.0.10 +beautifulsoup4 +django-tinymce + +# +scikit-learn + diff --git a/static/css/style.css b/static/css/main.css similarity index 67% rename from static/css/style.css rename to static/css/main.css index a243626..d67c824 100644 --- a/static/css/style.css +++ b/static/css/main.css @@ -1,9 +1,23 @@ +/* ========================================================================== + Base styles + ========================================================================== */ body { background-color: #f8f9fa; min-height: 100vh; font-family: 'Poppins', sans-serif; + margin: 0; + padding: 0; } +.container { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +/* ========================================================================== + Navigation + ========================================================================== */ .navbar { background-color: #343a40 !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); @@ -23,7 +37,9 @@ body { color: #007bff !important; } -/* Style cho container chứa form */ +/* ========================================================================== + Authentication Forms (Login/Register) + ========================================================================== */ .row.justify-content-center { min-height: calc(100vh - 76px); display: flex; @@ -48,6 +64,7 @@ body { z-index: -1; } +/* Card Styles */ .card { background-color: rgba(255, 255, 255, 0.95); border: 1px solid #dee2e6; @@ -70,6 +87,7 @@ body { font-size: 1.5rem; } +/* Form Elements */ .form-control { background-color: #fff; border: 1px solid #dee2e6; @@ -84,6 +102,14 @@ body { box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); } +.form-label { + color: #212529; + font-weight: 500; + margin-bottom: 6px; + font-size: 0.9rem; +} + +/* Buttons */ .btn-primary { background-color: #007bff; border: none; @@ -99,10 +125,17 @@ body { box-shadow: none; } -.form-label { - color: #212529; - font-weight: 500; - margin-bottom: 6px; +/* Alerts and Messages */ +.alert { + padding: 8px 12px; + margin-bottom: 16px; + border-radius: 4px; + font-size: 0.9rem; +} + +/* Typography */ +.text-muted { + color: #6c757d !important; font-size: 0.9rem; } @@ -117,22 +150,29 @@ body { text-decoration: underline; } -.alert { - padding: 8px 12px; - margin-bottom: 16px; - border-radius: 4px; - font-size: 0.9rem; -} - -.text-muted { - color: #6c757d !important; - font-size: 0.9rem; -} - +/* Spacing */ .mb-3 { margin-bottom: 0.8rem !important; } .mt-3 { margin-top: 0.8rem !important; +} + +/* ========================================================================== + Responsive Design + ========================================================================== */ +@media (max-width: 768px) { + .container { + width: 100%; + padding: 10px; + } + + .card { + margin: 10px; + } + + .row.justify-content-center { + padding: 10px; + } } \ No newline at end of file diff --git a/static/css/responsive.css b/static/css/responsive.css index be6c702..cfef865 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -16,3 +16,77 @@ footer { color: white; padding: 10px; } + +/* ========================================================================== + Responsive Design + ========================================================================== */ + +/* Extra small devices (phones, less than 576px) */ +@media (max-width: 575.98px) { + .container { + width: 100%; + padding: 8px; + } + + .card { + margin: 8px; + padding: 10px; + } + + .row.justify-content-center { + padding: 8px; + } + + .card-header h3 { + font-size: 1.2rem; + } + + .form-control, .btn { + font-size: 0.85rem; + } + + .navbar-brand { + font-size: 1.2rem; + } +} + +/* Small devices (landscape phones, 576px and up) */ +@media (min-width: 576px) and (max-width: 767.98px) { + .container { + width: 100%; + padding: 10px; + } + + .card { + margin: 10px; + } + + .row.justify-content-center { + padding: 10px; + } +} + +/* Medium devices (tablets, 768px and up) */ +@media (min-width: 768px) and (max-width: 991.98px) { + .container { + max-width: 720px; + } + + .card { + max-width: 600px; + } +} + +/* Large devices (desktops, 992px and up) */ +@media (min-width: 992px) and (max-width: 1199.98px) { + .container { + max-width: 960px; + } +} + +/* Extra large devices (large desktops, 1200px and up) */ +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} diff --git a/static/css/styles.css b/static/css/styles.css deleted file mode 100644 index 92e64ff..0000000 --- a/static/css/styles.css +++ /dev/null @@ -1,20 +0,0 @@ -/* Base styles */ -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; -} - -.container { - max-width: 800px; - margin: 0 auto; - padding: 20px; -} - -/* Responsive design */ -@media (max-width: 768px) { - .container { - width: 100%; - padding: 10px; - } -} diff --git a/static/js/ajax-load-more.js b/static/js/ajax-load-more.js new file mode 100644 index 0000000..152f3c7 --- /dev/null +++ b/static/js/ajax-load-more.js @@ -0,0 +1,58 @@ +document.addEventListener('DOMContentLoaded', function() { + // Các biến cần thiết + const loadMoreBtn = document.getElementById('load-more-btn'); + const newsContainer = document.getElementById('news-container'); + let page = 1; + + // Kiểm tra xem nút Load More có tồn tại không + if (loadMoreBtn) { + loadMoreBtn.addEventListener('click', function() { + // Tăng số trang + page++; + + // Hiển thị trạng thái đang tải + loadMoreBtn.innerHTML = ' Đang tải...'; + loadMoreBtn.disabled = true; + + // URL hiện tại (có thể có các tham số khác như feed=...) + const currentUrl = window.location.href; + // Tạo URL cho request Ajax + const ajaxUrl = `/load-more-news/?page=${page}` + + (currentUrl.includes('feed=') ? '&' + currentUrl.split('?')[1] : ''); + + // Thực hiện Ajax request + fetch(ajaxUrl) + .then(response => response.json()) + .then(data => { + // Thêm các bài viết mới vào container + if (data.html) { + // Thêm HTML mới vào cuối container + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = data.html; + + // Thêm từng phần tử con vào container + while (tempDiv.firstChild) { + newsContainer.appendChild(tempDiv.firstChild); + } + + // Khôi phục trạng thái nút + loadMoreBtn.innerHTML = 'Xem thêm'; + loadMoreBtn.disabled = false; + + // Ẩn nút nếu không còn bài viết + if (data.has_more === false) { + loadMoreBtn.style.display = 'none'; + } + } else { + // Ẩn nút nếu không có dữ liệu trả về + loadMoreBtn.style.display = 'none'; + } + }) + .catch(error => { + console.error('Lỗi khi tải thêm tin:', error); + loadMoreBtn.innerHTML = 'Thử lại'; + loadMoreBtn.disabled = false; + }); + }); + } +}); \ No newline at end of file diff --git a/static/my_admin/css/custom.css b/static/my_admin/css/custom.css new file mode 100644 index 0000000..d5f90bb --- /dev/null +++ b/static/my_admin/css/custom.css @@ -0,0 +1,3 @@ +#id_special, #id_is_homepage { + display: flex; +} \ No newline at end of file diff --git a/static/my_admin/js/general.js b/static/my_admin/js/general.js new file mode 100644 index 0000000..8eb7303 --- /dev/null +++ b/static/my_admin/js/general.js @@ -0,0 +1,8 @@ +$(document).ready(function() { + $('#id_name').on('input', function() { + let name = $(this).val(); + name = name.toLowerCase().replace(/đ/g, 'd'); + let slug = slugify(name).replace(/[^\w-]/g, ''); + $('#id_slug').val(slug); + }); +}); \ No newline at end of file diff --git a/static/my_admin/js/jquery-3.6.0.min.js b/static/my_admin/js/jquery-3.6.0.min.js new file mode 100644 index 0000000..963db5d --- /dev/null +++ b/static/my_admin/js/jquery-3.6.0.min.js @@ -0,0 +1,3 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0":"greater","|":"or","¢":"cent","£":"pound","¤":"currency","¥":"yen","©":"(c)","ª":"a","®":"(r)","º":"o","À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","Æ":"AE","Ç":"C","È":"E","É":"E","Ê":"E","Ë":"E","Ì":"I","Í":"I","Î":"I","Ï":"I","Ð":"D","Ñ":"N","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","Ù":"U","Ú":"U","Û":"U","Ü":"U","Ý":"Y","Þ":"TH","ß":"ss","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","æ":"ae","ç":"c","è":"e","é":"e","ê":"e","ë":"e","ì":"i","í":"i","î":"i","ï":"i","ð":"d","ñ":"n","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","ù":"u","ú":"u","û":"u","ü":"u","ý":"y","þ":"th","ÿ":"y","Ā":"A","ā":"a","Ă":"A","ă":"a","Ą":"A","ą":"a","Ć":"C","ć":"c","Č":"C","č":"c","Ď":"D","ď":"d","Đ":"DJ","đ":"dj","Ē":"E","ē":"e","Ė":"E","ė":"e","Ę":"e","ę":"e","Ě":"E","ě":"e","Ğ":"G","ğ":"g","Ģ":"G","ģ":"g","Ĩ":"I","ĩ":"i","Ī":"i","ī":"i","Į":"I","į":"i","İ":"I","ı":"i","Ķ":"k","ķ":"k","Ļ":"L","ļ":"l","Ľ":"L","ľ":"l","Ł":"L","ł":"l","Ń":"N","ń":"n","Ņ":"N","ņ":"n","Ň":"N","ň":"n","Ō":"O","ō":"o","Ő":"O","ő":"o","Œ":"OE","œ":"oe","Ŕ":"R","ŕ":"r","Ř":"R","ř":"r","Ś":"S","ś":"s","Ş":"S","ş":"s","Š":"S","š":"s","Ţ":"T","ţ":"t","Ť":"T","ť":"t","Ũ":"U","ũ":"u","Ū":"u","ū":"u","Ů":"U","ů":"u","Ű":"U","ű":"u","Ų":"U","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","ź":"z","Ż":"Z","ż":"z","Ž":"Z","ž":"z","Ə":"E","ƒ":"f","Ơ":"O","ơ":"o","Ư":"U","ư":"u","Lj":"LJ","lj":"lj","Nj":"NJ","nj":"nj","Ș":"S","ș":"s","Ț":"T","ț":"t","ə":"e","˚":"o","Ά":"A","Έ":"E","Ή":"H","Ί":"I","Ό":"O","Ύ":"Y","Ώ":"W","ΐ":"i","Α":"A","Β":"B","Γ":"G","Δ":"D","Ε":"E","Ζ":"Z","Η":"H","Θ":"8","Ι":"I","Κ":"K","Λ":"L","Μ":"M","Ν":"N","Ξ":"3","Ο":"O","Π":"P","Ρ":"R","Σ":"S","Τ":"T","Υ":"Y","Φ":"F","Χ":"X","Ψ":"PS","Ω":"W","Ϊ":"I","Ϋ":"Y","ά":"a","έ":"e","ή":"h","ί":"i","ΰ":"y","α":"a","β":"b","γ":"g","δ":"d","ε":"e","ζ":"z","η":"h","θ":"8","ι":"i","κ":"k","λ":"l","μ":"m","ν":"n","ξ":"3","ο":"o","π":"p","ρ":"r","ς":"s","σ":"s","τ":"t","υ":"y","φ":"f","χ":"x","ψ":"ps","ω":"w","ϊ":"i","ϋ":"y","ό":"o","ύ":"y","ώ":"w","Ё":"Yo","Ђ":"DJ","Є":"Ye","І":"I","Ї":"Yi","Ј":"J","Љ":"LJ","Њ":"NJ","Ћ":"C","Џ":"DZ","А":"A","Б":"B","В":"V","Г":"G","Д":"D","Е":"E","Ж":"Zh","З":"Z","И":"I","Й":"J","К":"K","Л":"L","М":"M","Н":"N","О":"O","П":"P","Р":"R","С":"S","Т":"T","У":"U","Ф":"F","Х":"H","Ц":"C","Ч":"Ch","Ш":"Sh","Щ":"Sh","Ъ":"U","Ы":"Y","Ь":"","Э":"E","Ю":"Yu","Я":"Ya","а":"a","б":"b","в":"v","г":"g","д":"d","е":"e","ж":"zh","з":"z","и":"i","й":"j","к":"k","л":"l","м":"m","н":"n","о":"o","п":"p","р":"r","с":"s","т":"t","у":"u","ф":"f","х":"h","ц":"c","ч":"ch","ш":"sh","щ":"sh","ъ":"u","ы":"y","ь":"","э":"e","ю":"yu","я":"ya","ё":"yo","ђ":"dj","є":"ye","і":"i","ї":"yi","ј":"j","љ":"lj","њ":"nj","ћ":"c","ѝ":"u","џ":"dz","Ґ":"G","ґ":"g","Ғ":"GH","ғ":"gh","Қ":"KH","қ":"kh","Ң":"NG","ң":"ng","Ү":"UE","ү":"ue","Ұ":"U","ұ":"u","Һ":"H","һ":"h","Ә":"AE","ә":"ae","Ө":"OE","ө":"oe","Ա":"A","Բ":"B","Գ":"G","Դ":"D","Ե":"E","Զ":"Z","Է":"E\'","Ը":"Y\'","Թ":"T\'","Ժ":"JH","Ի":"I","Լ":"L","Խ":"X","Ծ":"C\'","Կ":"K","Հ":"H","Ձ":"D\'","Ղ":"GH","Ճ":"TW","Մ":"M","Յ":"Y","Ն":"N","Շ":"SH","Չ":"CH","Պ":"P","Ջ":"J","Ռ":"R\'","Ս":"S","Վ":"V","Տ":"T","Ր":"R","Ց":"C","Փ":"P\'","Ք":"Q\'","Օ":"O\'\'","Ֆ":"F","և":"EV","ء":"a","آ":"aa","أ":"a","ؤ":"u","إ":"i","ئ":"e","ا":"a","ب":"b","ة":"h","ت":"t","ث":"th","ج":"j","ح":"h","خ":"kh","د":"d","ذ":"th","ر":"r","ز":"z","س":"s","ش":"sh","ص":"s","ض":"dh","ط":"t","ظ":"z","ع":"a","غ":"gh","ف":"f","ق":"q","ك":"k","ل":"l","م":"m","ن":"n","ه":"h","و":"w","ى":"a","ي":"y","ً":"an","ٌ":"on","ٍ":"en","َ":"a","ُ":"u","ِ":"e","ْ":"","٠":"0","١":"1","٢":"2","٣":"3","٤":"4","٥":"5","٦":"6","٧":"7","٨":"8","٩":"9","پ":"p","چ":"ch","ژ":"zh","ک":"k","گ":"g","ی":"y","۰":"0","۱":"1","۲":"2","۳":"3","۴":"4","۵":"5","۶":"6","۷":"7","۸":"8","۹":"9","฿":"baht","ა":"a","ბ":"b","გ":"g","დ":"d","ე":"e","ვ":"v","ზ":"z","თ":"t","ი":"i","კ":"k","ლ":"l","მ":"m","ნ":"n","ო":"o","პ":"p","ჟ":"zh","რ":"r","ს":"s","ტ":"t","უ":"u","ფ":"f","ქ":"k","ღ":"gh","ყ":"q","შ":"sh","ჩ":"ch","ც":"ts","ძ":"dz","წ":"ts","ჭ":"ch","ხ":"kh","ჯ":"j","ჰ":"h","Ṣ":"S","ṣ":"s","Ẁ":"W","ẁ":"w","Ẃ":"W","ẃ":"w","Ẅ":"W","ẅ":"w","ẞ":"SS","Ạ":"A","ạ":"a","Ả":"A","ả":"a","Ấ":"A","ấ":"a","Ầ":"A","ầ":"a","Ẩ":"A","ẩ":"a","Ẫ":"A","ẫ":"a","Ậ":"A","ậ":"a","Ắ":"A","ắ":"a","Ằ":"A","ằ":"a","Ẳ":"A","ẳ":"a","Ẵ":"A","ẵ":"a","Ặ":"A","ặ":"a","Ẹ":"E","ẹ":"e","Ẻ":"E","ẻ":"e","Ẽ":"E","ẽ":"e","Ế":"E","ế":"e","Ề":"E","ề":"e","Ể":"E","ể":"e","Ễ":"E","ễ":"e","Ệ":"E","ệ":"e","Ỉ":"I","ỉ":"i","Ị":"I","ị":"i","Ọ":"O","ọ":"o","Ỏ":"O","ỏ":"o","Ố":"O","ố":"o","Ồ":"O","ồ":"o","Ổ":"O","ổ":"o","Ỗ":"O","ỗ":"o","Ộ":"O","ộ":"o","Ớ":"O","ớ":"o","Ờ":"O","ờ":"o","Ở":"O","ở":"o","Ỡ":"O","ỡ":"o","Ợ":"O","ợ":"o","Ụ":"U","ụ":"u","Ủ":"U","ủ":"u","Ứ":"U","ứ":"u","Ừ":"U","ừ":"u","Ử":"U","ử":"u","Ữ":"U","ữ":"u","Ự":"U","ự":"u","Ỳ":"Y","ỳ":"y","Ỵ":"Y","ỵ":"y","Ỷ":"Y","ỷ":"y","Ỹ":"Y","ỹ":"y","–":"-","‘":"\'","’":"\'","“":"\\"","”":"\\"","„":"\\"","†":"+","•":"*","…":"...","₠":"ecu","₢":"cruzeiro","₣":"french franc","₤":"lira","₥":"mill","₦":"naira","₧":"peseta","₨":"rupee","₩":"won","₪":"new shequel","₫":"dong","€":"euro","₭":"kip","₮":"tugrik","₯":"drachma","₰":"penny","₱":"peso","₲":"guarani","₳":"austral","₴":"hryvnia","₵":"cedi","₸":"kazakhstani tenge","₹":"indian rupee","₺":"turkish lira","₽":"russian ruble","₿":"bitcoin","℠":"sm","™":"tm","∂":"d","∆":"delta","∑":"sum","∞":"infinity","♥":"love","元":"yuan","円":"yen","﷼":"rial","ﻵ":"laa","ﻷ":"laa","ﻹ":"lai","ﻻ":"la"}'),a=JSON.parse('{"bg":{"Й":"Y","Ц":"Ts","Щ":"Sht","Ъ":"A","Ь":"Y","й":"y","ц":"ts","щ":"sht","ъ":"a","ь":"y"},"de":{"Ä":"AE","ä":"ae","Ö":"OE","ö":"oe","Ü":"UE","ü":"ue","ß":"ss","%":"prozent","&":"und","|":"oder","∑":"summe","∞":"unendlich","♥":"liebe"},"es":{"%":"por ciento","&":"y","<":"menor que",">":"mayor que","|":"o","¢":"centavos","£":"libras","¤":"moneda","₣":"francos","∑":"suma","∞":"infinito","♥":"amor"},"fr":{"%":"pourcent","&":"et","<":"plus petit",">":"plus grand","|":"ou","¢":"centime","£":"livre","¤":"devise","₣":"franc","∑":"somme","∞":"infini","♥":"amour"},"pt":{"%":"porcento","&":"e","<":"menor",">":"maior","|":"ou","¢":"centavo","∑":"soma","£":"libra","∞":"infinito","♥":"amor"},"uk":{"И":"Y","и":"y","Й":"Y","й":"y","Ц":"Ts","ц":"ts","Х":"Kh","х":"kh","Щ":"Shch","щ":"shch","Г":"H","г":"h"},"vi":{"Đ":"D","đ":"d"},"da":{"Ø":"OE","ø":"oe","Å":"AA","å":"aa","%":"procent","&":"og","|":"eller","$":"dollar","<":"mindre end",">":"større end"},"nb":{"&":"og","Å":"AA","Æ":"AE","Ø":"OE","å":"aa","æ":"ae","ø":"oe"},"it":{"&":"e"},"nl":{"&":"en"},"sv":{"&":"och","Å":"AA","Ä":"AE","Ö":"OE","å":"aa","ä":"ae","ö":"oe"}}');function o(o,n){if("string"!=typeof o)throw new Error("slugify: string argument expected");var r=a[(n="string"==typeof n?{replacement:n}:n||{}).locale]||{},i=void 0===n.replacement?"-":n.replacement,t=void 0===n.trim||n.trim,u=o.normalize().split("").reduce((function(a,o){var t=r[o];return void 0===t&&(t=e[o]),void 0===t&&(t=o),t===i&&(t=" "),a+t.replace(n.remove||/[^\w\s$*_+~.()'"!\-:@]+/g,"")}),"");return n.strict&&(u=u.replace(/[^A-Za-z0-9\s]/g,"")),t&&(u=u.trim()),u=u.replace(/\s+/g,i),n.lower&&(u=u.toLowerCase()),u}return o.extend=function(a){Object.assign(e,a)},o})); +//# sourceMappingURL=/sm/69d569b95b6cfd595c2186547d9d7497cb1ac2392b71f77d53ac49ea551ff5b4.map +1 \ No newline at end of file diff --git a/static/news_app/image/1_avatar.png b/static/news_app/image/1_avatar.png new file mode 100644 index 0000000..02c9ae0 Binary files /dev/null and b/static/news_app/image/1_avatar.png differ diff --git a/static/news_app/image/2_avatar.png b/static/news_app/image/2_avatar.png new file mode 100644 index 0000000..9b3171e Binary files /dev/null and b/static/news_app/image/2_avatar.png differ diff --git a/static/news_app/image/3_avatar.png b/static/news_app/image/3_avatar.png new file mode 100644 index 0000000..e738f32 Binary files /dev/null and b/static/news_app/image/3_avatar.png differ diff --git a/static/news_app/image/article/Anh_chup_man_hinh_1.png b/static/news_app/image/article/Anh_chup_man_hinh_1.png new file mode 100644 index 0000000..b58bd72 Binary files /dev/null and b/static/news_app/image/article/Anh_chup_man_hinh_1.png differ diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html new file mode 100644 index 0000000..f025d82 --- /dev/null +++ b/templates/admin/base_site.html @@ -0,0 +1,42 @@ +{% extends "admin/base.html" %} + +{% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} + +{% block extrahead %} + +{% endblock %} + +{% block branding %} +

{{ site_header|default:_('Django administration') }}

+{% endblock %} + +{% block nav-global %}{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index c4905ca..5e1b13f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,50 +6,170 @@ Welcome + {% load static %} -