Compare commits
9 Commits
lzc_branch
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
6cd016724b | 2 months ago |
|
|
aabee0c532 | 2 months ago |
|
|
da100ebbb4 | 2 months ago |
|
|
dcd2a18201 | 3 months ago |
|
|
c30e2c70ea | 3 months ago |
|
|
2355416174 | 3 months ago |
|
|
889314d012 | 3 months ago |
|
|
70e7d17381 | 3 months ago |
|
|
d84404c3a0 | 3 months ago |
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.11 (PythonProject)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (PythonProject)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (PythonProject)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/djiango" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/my-project" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,16 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from .models import Flower, FlowerCategory, CultivationTip, Carousel
|
||||
|
||||
@admin.register(Flower)
|
||||
class FlowerAdmin(admin.ModelAdmin):
|
||||
list_display = ['name', 'category', 'difficulty', 'created_at']
|
||||
list_filter = ['category', 'difficulty']
|
||||
search_fields = ['name', 'scientific_name']
|
||||
|
||||
@admin.register(Carousel)
|
||||
class CarouselAdmin(admin.ModelAdmin):
|
||||
list_display = ['title', 'sort_order', 'is_active']
|
||||
list_editable = ['sort_order', 'is_active']
|
||||
|
||||
admin.site.register(FlowerCategory)
|
||||
admin.site.register(CultivationTip)
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BlogConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'blog'
|
||||
@ -1,55 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-12 09:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
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)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Post',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('content', models.TextField()),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='post_images/')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='blog.category')),
|
||||
('tags', models.ManyToManyField(blank=True, to='blog.tag')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='blog.post')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -1,101 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-12 10:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Flower',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='花卉名称')),
|
||||
('scientific_name', models.CharField(blank=True, max_length=200, verbose_name='学名')),
|
||||
('description', models.TextField(verbose_name='花卉描述')),
|
||||
('cultivation_techniques', models.TextField(verbose_name='养殖技巧')),
|
||||
('watering_frequency', models.CharField(max_length=100, verbose_name='浇水频率')),
|
||||
('sunlight_requirements', models.CharField(max_length=100, verbose_name='光照需求')),
|
||||
('temperature_range', models.CharField(max_length=100, verbose_name='适宜温度')),
|
||||
('difficulty', models.CharField(choices=[('easy', '简单'), ('medium', '中等'), ('hard', '困难')], max_length=10, verbose_name='养殖难度')),
|
||||
('image', models.ImageField(blank=True, upload_to='flowers/', verbose_name='花卉图片')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '花卉信息',
|
||||
'verbose_name_plural': '花卉信息',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FlowerCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, verbose_name='分类名称')),
|
||||
('description', models.TextField(blank=True, verbose_name='分类描述')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '花卉分类',
|
||||
'verbose_name_plural': '花卉分类',
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='category',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='author',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='post',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='author',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='post',
|
||||
name='tags',
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CultivationTip',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='技巧标题')),
|
||||
('content', models.TextField(verbose_name='技巧内容')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('flower', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tips', to='blog.flower')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '养殖技巧',
|
||||
'verbose_name_plural': '养殖技巧',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='flower',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.flowercategory', verbose_name='分类'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Category',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Comment',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Post',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Tag',
|
||||
),
|
||||
]
|
||||
@ -1,50 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-12 11:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('blog', '0002_flower_flowercategory_remove_post_category_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Carousel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200, verbose_name='标题')),
|
||||
('subtitle', models.CharField(blank=True, max_length=200, verbose_name='副标题')),
|
||||
('image', models.ImageField(upload_to='carousel/', verbose_name='轮播图片')),
|
||||
('link', models.CharField(blank=True, max_length=200, verbose_name='链接')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='是否激活')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '轮播图',
|
||||
'verbose_name_plural': '轮播图',
|
||||
'ordering': ['sort_order'],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='flowercategory',
|
||||
options={'ordering': ['sort_order', 'name'], 'verbose_name': '花卉分类', 'verbose_name_plural': '花卉分类'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='flowercategory',
|
||||
name='category_type',
|
||||
field=models.CharField(choices=[('indoor', '室内植物'), ('outdoor', '室外植物'), ('succulent', '多肉植物'), ('flowering', '观花植物'), ('foliage', '观叶植物'), ('herb', '草本植物')], default='indoor', max_length=20, verbose_name='分类类型'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='flowercategory',
|
||||
name='icon',
|
||||
field=models.CharField(default='🌿', max_length=50, verbose_name='分类图标'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='flowercategory',
|
||||
name='sort_order',
|
||||
field=models.IntegerField(default=0, verbose_name='排序'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,88 +0,0 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class FlowerCategory(models.Model):
|
||||
"""花卉分类模型"""
|
||||
CATEGORY_TYPES = [
|
||||
('indoor', '室内植物'),
|
||||
('outdoor', '室外植物'),
|
||||
('succulent', '多肉植物'),
|
||||
('flowering', '观花植物'),
|
||||
('foliage', '观叶植物'),
|
||||
('herb', '草本植物'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, verbose_name="分类名称")
|
||||
category_type = models.CharField(max_length=20, choices=CATEGORY_TYPES, verbose_name="分类类型")
|
||||
description = models.TextField(blank=True, verbose_name="分类描述")
|
||||
icon = models.CharField(max_length=50, default='🌿', verbose_name="分类图标")
|
||||
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "花卉分类"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort_order', 'name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Carousel(models.Model):
|
||||
"""轮播图模型"""
|
||||
title = models.CharField(max_length=200, verbose_name="标题")
|
||||
subtitle = models.CharField(max_length=200, blank=True, verbose_name="副标题")
|
||||
image = models.ImageField(upload_to='carousel/', verbose_name="轮播图片")
|
||||
link = models.CharField(max_length=200, blank=True, verbose_name="链接")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否激活")
|
||||
sort_order = models.IntegerField(default=0, verbose_name="排序")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "轮播图"
|
||||
verbose_name_plural = verbose_name
|
||||
ordering = ['sort_order']
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Flower(models.Model):
|
||||
"""花卉信息"""
|
||||
DIFFICULTY_LEVEL = [
|
||||
('easy', '简单'),
|
||||
('medium', '中等'),
|
||||
('hard', '困难'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=200, verbose_name="花卉名称")
|
||||
scientific_name = models.CharField(max_length=200, verbose_name="学名", blank=True)
|
||||
category = models.ForeignKey(FlowerCategory, on_delete=models.CASCADE, verbose_name="分类")
|
||||
description = models.TextField(verbose_name="花卉描述")
|
||||
cultivation_techniques = models.TextField(verbose_name="养殖技巧")
|
||||
watering_frequency = models.CharField(max_length=100, verbose_name="浇水频率")
|
||||
sunlight_requirements = models.CharField(max_length=100, verbose_name="光照需求")
|
||||
temperature_range = models.CharField(max_length=100, verbose_name="适宜温度")
|
||||
difficulty = models.CharField(max_length=10, choices=DIFFICULTY_LEVEL, verbose_name="养殖难度")
|
||||
image = models.ImageField(upload_to='flowers/', verbose_name="花卉图片", blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "花卉信息"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class CultivationTip(models.Model):
|
||||
"""养殖技巧"""
|
||||
flower = models.ForeignKey(Flower, on_delete=models.CASCADE, related_name='tips')
|
||||
title = models.CharField(max_length=200, verbose_name="技巧标题")
|
||||
content = models.TextField(verbose_name="技巧内容")
|
||||
author = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "养殖技巧"
|
||||
verbose_name_plural = verbose_name
|
||||
@ -1,41 +0,0 @@
|
||||
<!-- blog/templates/blog/base.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}花卉养殖平台{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.0/font/bootstrap-icons.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
color: #2c7873;
|
||||
}
|
||||
.sidebar-widget {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% include 'blog/includes/header.html' %}
|
||||
|
||||
<main class="container my-4">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% include 'blog/includes/footer.html' %}
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,34 +0,0 @@
|
||||
{% extends 'blog/base.html' %}
|
||||
|
||||
{% block title %}花卉图鉴 - 花卉养殖平台{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="mb-4">花卉图鉴</h1>
|
||||
|
||||
<div class="row">
|
||||
{% for flower in flowers %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
{% include 'blog/includes/flower_card.html' with flower=flower %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">上一页</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">下一页</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -1,18 +0,0 @@
|
||||
<div class="article-card h-100">
|
||||
{% if flower.image %}
|
||||
<img src="{{ flower.image.url }}" class="card-img-top" alt="{{ flower.name }}"
|
||||
style="height: 200px; object-fit: cover;">
|
||||
{% endif %}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ flower.name }}</h5>
|
||||
<p class="card-text text-muted">{{ flower.description|truncatewords:15 }}</p>
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-success">{{ flower.category.name }}</span>
|
||||
<span class="badge bg-info">{{ flower.get_difficulty_display }}</span>
|
||||
</div>
|
||||
<a href="{% url 'blog:flower_detail' flower.pk %}" class="read-more">查看养殖方法 →</a>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
<small>发布于 {{ flower.created_at|date:"Y-m-d" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@ -1,36 +0,0 @@
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'blog:home' %}">
|
||||
🌸 花卉养殖平台
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'blog:home' %}">首页</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'blog:flower_list' %}">花卉图鉴</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">养殖技巧</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">病虫害防治</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">季节养护</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="d-flex">
|
||||
<a href="#" class="btn btn-outline-success me-2">登录</a>
|
||||
<a href="#" class="btn btn-success">注册</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@ -1,46 +0,0 @@
|
||||
<!-- blog/templates/blog/includes/sidebar.html -->
|
||||
<!-- 搜索花卉 -->
|
||||
<div class="sidebar-widget">
|
||||
<h5 class="mb-3">🔍 搜索花卉</h5>
|
||||
<form class="d-flex" method="get" action="{% url 'blog:flower_list' %}">
|
||||
<input class="form-control me-2" type="search" name="q" placeholder="输入花卉名称..." required>
|
||||
<button class="btn btn-success" type="submit">搜索</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 花卉分类 -->
|
||||
<div class="sidebar-widget">
|
||||
<h5 class="mb-3">📂 花卉分类</h5>
|
||||
<div class="list-group">
|
||||
{% for category in categories %}
|
||||
<a href="{% url 'blog:flower_list' %}?category={{ category.id }}"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
{{ category.icon }} {{ category.name }}
|
||||
<span class="badge bg-primary rounded-pill">{{ category.flower_set.count }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 养殖难度 -->
|
||||
<div class="sidebar-widget">
|
||||
<h5 class="mb-3">⚡ 养殖难度</h5>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{% url 'blog:flower_list' %}?difficulty=easy" class="btn btn-sm btn-outline-success">简单</a>
|
||||
<a href="{% url 'blog:flower_list' %}?difficulty=medium" class="btn btn-sm btn-outline-warning">中等</a>
|
||||
<a href="{% url 'blog:flower_list' %}?difficulty=hard" class="btn btn-sm btn-outline-danger">困难</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热门标签 -->
|
||||
<div class="sidebar-widget">
|
||||
<h5 class="mb-3">🏷️ 热门标签</h5>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<span class="badge bg-light text-dark">多肉植物</span>
|
||||
<span class="badge bg-light text-dark">观花植物</span>
|
||||
<span class="badge bg-light text-dark">室内养护</span>
|
||||
<span class="badge bg-light text-dark">浇水技巧</span>
|
||||
<span class="badge bg-light text-dark">施肥方法</span>
|
||||
<span class="badge bg-light text-dark">病虫害防治</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -1,10 +0,0 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'blog'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
path('flowers/', views.FlowerListView.as_view(), name='flower_list'),
|
||||
path('flower/<int:pk>/', views.FlowerDetailView.as_view(), name='flower_detail'),
|
||||
]
|
||||
@ -1,52 +0,0 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic import ListView, DetailView
|
||||
from .models import Flower, FlowerCategory,Carousel, CultivationTip
|
||||
|
||||
|
||||
class FlowerListView(ListView):
|
||||
"""花卉列表页"""
|
||||
model = Flower
|
||||
template_name = 'blog/flower_list.html'
|
||||
context_object_name = 'flowers'
|
||||
paginate_by = 12
|
||||
|
||||
def get_queryset(self):
|
||||
return Flower.objects.all().order_by('-created_at')
|
||||
|
||||
|
||||
class FlowerDetailView(DetailView):
|
||||
"""花卉详情页"""
|
||||
model = Flower
|
||||
template_name = 'blog/flower_detail.html'
|
||||
context_object_name = 'flower'
|
||||
|
||||
|
||||
def home(request):
|
||||
# 获取轮播图数据
|
||||
carousel_items = Carousel.objects.filter(is_active=True).order_by('sort_order')[:5]
|
||||
|
||||
# 获取分类数据
|
||||
categories = FlowerCategory.objects.all()
|
||||
|
||||
# 获取最新花卉
|
||||
recent_flowers = Flower.objects.all().order_by('-created_at')[:6]
|
||||
|
||||
# 按分类获取热门花卉
|
||||
featured_flowers = {}
|
||||
for category in categories[:4]: # 只取前4个分类
|
||||
flowers = Flower.objects.filter(category=category)[:3]
|
||||
if flowers:
|
||||
featured_flowers[category.name] = flowers
|
||||
|
||||
context = {
|
||||
'carousel_items': carousel_items,
|
||||
'categories': categories,
|
||||
'recent_flowers': recent_flowers,
|
||||
'featured_flowers': featured_flowers,
|
||||
}
|
||||
return render(request, 'blog/home.html', context)
|
||||
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Binary file not shown.
@ -1 +0,0 @@
|
||||
Subproject commit 5b7cd67489f56f83b8b880b078a311a0c0188a0a
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,16 +0,0 @@
|
||||
"""
|
||||
ASGI config for flower_blog project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'flower_blog.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
@ -1,129 +0,0 @@
|
||||
"""
|
||||
Django settings for flower_blog project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.2.7.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-%is$0jt=!!h-i7e#5knvw+@mqs_14bw1qq#6_yterbb6(f5l-8'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'blog',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'flower_blog.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'flower_blog.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
import os
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'blog/static')]
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
@ -1,30 +0,0 @@
|
||||
"""
|
||||
URL configuration for flower_blog project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('blog.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
@ -1,16 +0,0 @@
|
||||
"""
|
||||
WSGI config for flower_blog project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'flower_blog.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
@ -1,22 +0,0 @@
|
||||
#!/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', 'flower_blog.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit fbc201dcbbb6029c78295ecc656ae484794ef960
|
||||
@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
@ -0,0 +1,176 @@
|
||||
name: 自动部署到生产环境
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Django CI"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: '部署环境'
|
||||
required: true
|
||||
default: 'production'
|
||||
type: choice
|
||||
options:
|
||||
- production
|
||||
- staging
|
||||
image_tag:
|
||||
description: '镜像标签 (默认: latest)'
|
||||
required: false
|
||||
default: 'latest'
|
||||
type: string
|
||||
skip_tests:
|
||||
description: '跳过测试直接部署'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
REGISTRY: registry.cn-shenzhen.aliyuncs.com
|
||||
IMAGE_NAME: liangliangyy/djangoblog
|
||||
NAMESPACE: djangoblog
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: 构建镜像并部署到生产环境
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置部署参数
|
||||
id: deploy-params
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
|
||||
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
|
||||
echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
|
||||
echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
|
||||
echo "environment=production" >> $GITHUB_OUTPUT
|
||||
echo "image_tag=latest" >> $GITHUB_OUTPUT
|
||||
echo "skip_tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: 显示部署信息
|
||||
run: |
|
||||
echo "🚀 部署信息:"
|
||||
echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
|
||||
echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
|
||||
echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
|
||||
echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
|
||||
|
||||
- name: 设置Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 登录私有镜像仓库
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: 提取镜像元数据
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=sha,prefix={{branch}}-
|
||||
type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
|
||||
|
||||
- name: 构建并推送Docker镜像
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: 部署到生产服务器
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.PRODUCTION_HOST }}
|
||||
username: ${{ secrets.PRODUCTION_USER }}
|
||||
key: ${{ secrets.PRODUCTION_SSH_KEY }}
|
||||
port: ${{ secrets.PRODUCTION_PORT || 22 }}
|
||||
script: |
|
||||
echo "🚀 开始部署 DjangoBlog..."
|
||||
|
||||
# 检查kubectl是否可用
|
||||
if ! command -v kubectl &> /dev/null; then
|
||||
echo "❌ 错误: kubectl 未安装或不在PATH中"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查命名空间是否存在
|
||||
if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
|
||||
echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 更新deployment镜像
|
||||
echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
|
||||
kubectl set image deployment/djangoblog \
|
||||
djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
|
||||
-n ${{ env.NAMESPACE }}
|
||||
|
||||
# 重启deployment
|
||||
echo "🔄 重启deployment..."
|
||||
kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
|
||||
|
||||
# 等待deployment完成
|
||||
echo "⏳ 等待deployment完成..."
|
||||
kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
|
||||
|
||||
# 检查deployment状态
|
||||
echo "✅ 检查deployment状态..."
|
||||
kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
|
||||
kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
|
||||
|
||||
echo "🎉 部署完成!"
|
||||
|
||||
- name: 发送部署通知
|
||||
if: always()
|
||||
run: |
|
||||
# 设置通知内容
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
TITLE="✅ DjangoBlog部署成功"
|
||||
STATUS="成功"
|
||||
else
|
||||
TITLE="❌ DjangoBlog部署失败"
|
||||
STATUS="失败"
|
||||
fi
|
||||
|
||||
MESSAGE="部署状态: ${STATUS}
|
||||
触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
|
||||
部署环境: ${{ steps.deploy-params.outputs.environment }}
|
||||
镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
|
||||
提交者: ${{ github.actor }}
|
||||
时间: $(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
# 发送Server酱通知
|
||||
if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
|
||||
echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
|
||||
|
||||
curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data @/tmp/serverchan.json \
|
||||
--silent > /dev/null
|
||||
|
||||
rm -f /tmp/serverchan.json
|
||||
echo "📱 部署通知已发送"
|
||||
fi
|
||||
@ -0,0 +1,371 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# 标准测试 - Python 3.10
|
||||
- python-version: "3.10"
|
||||
test-type: "standard"
|
||||
database: "mysql"
|
||||
elasticsearch: false
|
||||
coverage: false
|
||||
|
||||
# 标准测试 - Python 3.11
|
||||
- python-version: "3.11"
|
||||
test-type: "standard"
|
||||
database: "mysql"
|
||||
elasticsearch: false
|
||||
coverage: false
|
||||
|
||||
# 完整测试 - 包含ES和覆盖率
|
||||
- python-version: "3.11"
|
||||
test-type: "full"
|
||||
database: "mysql"
|
||||
elasticsearch: true
|
||||
coverage: true
|
||||
|
||||
# Docker构建测试
|
||||
- python-version: "3.11"
|
||||
test-type: "docker"
|
||||
database: "none"
|
||||
elasticsearch: false
|
||||
coverage: false
|
||||
|
||||
name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
|
||||
|
||||
steps:
|
||||
- name: Checkout代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置测试信息
|
||||
id: test-info
|
||||
run: |
|
||||
echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
|
||||
if [ "${{ matrix.test-type }}" = "docker" ]; then
|
||||
echo "skip_python_setup=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip_python_setup=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# MySQL数据库设置 (只有需要数据库的测试才执行)
|
||||
- name: 启动MySQL数据库
|
||||
if: matrix.database == 'mysql'
|
||||
uses: samin/mysql-action@v1.3
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
character set server: utf8mb4
|
||||
collation server: utf8mb4_general_ci
|
||||
mysql version: latest
|
||||
mysql root password: root
|
||||
mysql database: djangoblog
|
||||
mysql user: root
|
||||
mysql password: root
|
||||
|
||||
# Elasticsearch设置 (只有完整测试才执行)
|
||||
- name: 配置系统参数 (ES)
|
||||
if: matrix.elasticsearch == true
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo sysctl -w vm.swappiness=1
|
||||
sudo sysctl -w fs.file-max=262144
|
||||
sudo sysctl -w vm.max_map_count=262144
|
||||
|
||||
- name: 启动Elasticsearch
|
||||
if: matrix.elasticsearch == true
|
||||
uses: miyataka/elasticsearch-github-actions@1
|
||||
with:
|
||||
stack-version: '7.12.1'
|
||||
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
|
||||
|
||||
# Python环境设置 (Docker测试跳过)
|
||||
- name: 设置Python ${{ matrix.python-version }}
|
||||
if: steps.test-info.outputs.skip_python_setup == 'false'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'requirements.txt'
|
||||
|
||||
# 多层缓存策略优化
|
||||
- name: 缓存Python依赖
|
||||
if: steps.test-info.outputs.skip_python_setup == 'false'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/pip
|
||||
.pytest_cache
|
||||
key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
|
||||
${{ runner.os }}-python-${{ matrix.python-version }}-
|
||||
${{ runner.os }}-python-
|
||||
|
||||
# Django缓存优化 (测试数据库等)
|
||||
- name: 缓存Django资源
|
||||
if: matrix.test-type != 'docker'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.django_cache/
|
||||
key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-django-${{ matrix.test-type }}-
|
||||
${{ runner.os }}-django-
|
||||
|
||||
- name: 安装Python依赖
|
||||
if: steps.test-info.outputs.skip_python_setup == 'false'
|
||||
run: |
|
||||
echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
# 安装基础依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 根据测试类型安装额外依赖
|
||||
if [ "${{ matrix.coverage }}" = "true" ]; then
|
||||
echo "📊 安装覆盖率工具"
|
||||
pip install coverage[toml]
|
||||
fi
|
||||
|
||||
# 验证关键依赖
|
||||
echo "🔍 验证关键依赖安装"
|
||||
python -c "import django; print(f'Django version: {django.get_version()}')"
|
||||
python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
|
||||
|
||||
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
|
||||
python -c "import elasticsearch; print('Elasticsearch client: OK')"
|
||||
fi
|
||||
|
||||
# Django环境准备
|
||||
- name: 准备Django环境
|
||||
if: matrix.test-type != 'docker'
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
|
||||
run: |
|
||||
echo "🔧 准备Django测试环境"
|
||||
|
||||
# 等待数据库就绪
|
||||
echo "⏳ 等待MySQL数据库启动..."
|
||||
for i in {1..30}; do
|
||||
if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
|
||||
echo "✅ MySQL数据库连接成功"
|
||||
break
|
||||
fi
|
||||
echo "🔄 等待数据库启动... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# 等待Elasticsearch就绪 (如果启用)
|
||||
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
|
||||
echo "⏳ 等待Elasticsearch启动..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
|
||||
echo "✅ Elasticsearch连接成功"
|
||||
break
|
||||
fi
|
||||
echo "🔄 等待Elasticsearch启动... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
fi
|
||||
|
||||
# Django测试执行
|
||||
- name: 执行数据库迁移
|
||||
if: matrix.test-type != 'docker'
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
|
||||
run: |
|
||||
echo "🗄️ 执行数据库迁移"
|
||||
|
||||
# 检查迁移文件
|
||||
echo "📋 检查待应用的迁移..."
|
||||
python manage.py showmigrations
|
||||
|
||||
# 检查是否有未创建的迁移
|
||||
python manage.py makemigrations --check --verbosity 2
|
||||
|
||||
# 执行迁移
|
||||
python manage.py migrate --verbosity 2
|
||||
|
||||
echo "✅ 数据库迁移完成"
|
||||
|
||||
- name: 运行Django测试
|
||||
if: matrix.test-type != 'docker'
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
|
||||
run: |
|
||||
echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
|
||||
|
||||
# 显示Django配置信息
|
||||
python manage.py diffsettings | head -20
|
||||
|
||||
# 运行测试
|
||||
if [ "${{ matrix.coverage }}" = "true" ]; then
|
||||
echo "📊 运行测试并生成覆盖率报告"
|
||||
coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
|
||||
|
||||
echo "📈 生成覆盖率报告"
|
||||
coverage xml
|
||||
coverage report --show-missing
|
||||
coverage html
|
||||
|
||||
echo "📋 覆盖率统计:"
|
||||
coverage report | tail -1
|
||||
else
|
||||
echo "🧪 运行标准测试"
|
||||
python manage.py test --verbosity=2 --failfast
|
||||
fi
|
||||
|
||||
echo "✅ 测试执行完成"
|
||||
|
||||
# 覆盖率报告上传 (只有完整测试才执行)
|
||||
- name: 上传覆盖率到Codecov
|
||||
if: matrix.coverage == true && success()
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-${{ steps.test-info.outputs.test_name }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
- name: 上传覆盖率到Codecov (备用)
|
||||
if: matrix.coverage == true && failure()
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
# Docker构建测试
|
||||
- name: 设置QEMU
|
||||
if: matrix.test-type == 'docker'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: 设置Docker Buildx
|
||||
if: matrix.test-type == 'docker'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker构建测试
|
||||
if: matrix.test-type == 'docker'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: djangoblog/djangoblog:test-${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# 收集测试工件 (失败时收集调试信息)
|
||||
- name: 收集测试工件
|
||||
if: failure() && matrix.test-type != 'docker'
|
||||
run: |
|
||||
echo "🔍 收集测试失败的调试信息"
|
||||
|
||||
# 收集Django日志
|
||||
if [ -d "logs" ]; then
|
||||
echo "📄 Django日志文件:"
|
||||
ls -la logs/
|
||||
if [ -f "logs/djangoblog.log" ]; then
|
||||
echo "🔍 最新日志内容:"
|
||||
tail -100 logs/djangoblog.log
|
||||
fi
|
||||
fi
|
||||
|
||||
# 显示数据库状态
|
||||
echo "🗄️ 数据库连接状态:"
|
||||
python -c "
|
||||
try:
|
||||
from django.db import connection
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT VERSION()')
|
||||
print(f'MySQL版本: {cursor.fetchone()[0]}')
|
||||
cursor.execute('SHOW TABLES')
|
||||
tables = cursor.fetchall()
|
||||
print(f'数据库表数量: {len(tables)}')
|
||||
except Exception as e:
|
||||
print(f'数据库连接错误: {e}')
|
||||
" || true
|
||||
|
||||
# Elasticsearch状态 (如果启用)
|
||||
if [ "${{ matrix.elasticsearch }}" = "true" ]; then
|
||||
echo "🔍 Elasticsearch状态:"
|
||||
curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
|
||||
fi
|
||||
|
||||
# 上传测试工件
|
||||
- name: 上传覆盖率HTML报告
|
||||
if: matrix.coverage == true && always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-report-${{ steps.test-info.outputs.test_name }}
|
||||
path: htmlcov/
|
||||
retention-days: 30
|
||||
|
||||
# 性能统计
|
||||
- name: 测试性能统计
|
||||
if: always() && matrix.test-type != 'docker'
|
||||
run: |
|
||||
echo "⚡ 测试性能统计:"
|
||||
echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
|
||||
echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
|
||||
# 系统资源使用情况
|
||||
echo "💻 系统资源:"
|
||||
echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
|
||||
echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
|
||||
echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
|
||||
|
||||
# 测试结果汇总
|
||||
- name: 测试完成总结
|
||||
if: always()
|
||||
run: |
|
||||
echo "📋 ============ 测试执行总结 ============"
|
||||
echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
|
||||
echo " 🐍 Python版本: ${{ matrix.python-version }}"
|
||||
echo " 🗄️ 数据库: ${{ matrix.database }}"
|
||||
echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
|
||||
echo " 📊 覆盖率: ${{ matrix.coverage }}"
|
||||
echo " ⚡ 状态: ${{ job.status }}"
|
||||
echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
|
||||
# 根据测试结果显示不同消息
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
echo "🎉 测试执行成功!"
|
||||
else
|
||||
echo "❌ 测试执行失败,请检查上面的日志"
|
||||
fi
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -1,10 +0,0 @@
|
||||
[run]
|
||||
source = .
|
||||
include = *.py
|
||||
omit =
|
||||
*migrations*
|
||||
*tests*
|
||||
*.html
|
||||
*whoosh_cn_backend*
|
||||
*settings.py*
|
||||
*venv*
|
||||
@ -1,136 +0,0 @@
|
||||
name: Django CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
|
||||
jobs:
|
||||
build-normal:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: ["3.10","3.11" ]
|
||||
|
||||
steps:
|
||||
- name: Start MySQL
|
||||
uses: samin/mysql-action@v1.3
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
character set server: utf8mb4
|
||||
collation server: utf8mb4_general_ci
|
||||
mysql version: latest
|
||||
mysql root password: root
|
||||
mysql database: djangoblog
|
||||
mysql user: root
|
||||
mysql password: root
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Run Tests
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
run: |
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
python manage.py test
|
||||
|
||||
build-with-es:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: ["3.10","3.11" ]
|
||||
|
||||
steps:
|
||||
- name: Start MySQL
|
||||
uses: samin/mysql-action@v1.3
|
||||
with:
|
||||
host port: 3306
|
||||
container port: 3306
|
||||
character set server: utf8mb4
|
||||
collation server: utf8mb4_general_ci
|
||||
mysql version: latest
|
||||
mysql root password: root
|
||||
mysql database: djangoblog
|
||||
mysql user: root
|
||||
mysql password: root
|
||||
|
||||
- name: Configure sysctl limits
|
||||
run: |
|
||||
sudo swapoff -a
|
||||
sudo sysctl -w vm.swappiness=1
|
||||
sudo sysctl -w fs.file-max=262144
|
||||
sudo sysctl -w vm.max_map_count=262144
|
||||
|
||||
- uses: miyataka/elasticsearch-github-actions@1
|
||||
|
||||
with:
|
||||
stack-version: '7.12.1'
|
||||
plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
|
||||
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Run Tests
|
||||
env:
|
||||
DJANGO_MYSQL_PASSWORD: root
|
||||
DJANGO_MYSQL_HOST: 127.0.0.1
|
||||
DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
|
||||
run: |
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
coverage run manage.py test
|
||||
coverage xml
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
tags: djangoblog/djangoblog:dev
|
||||
@ -1,41 +0,0 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BasePlugin:
|
||||
# 插件元数据
|
||||
PLUGIN_NAME = None
|
||||
PLUGIN_DESCRIPTION = None
|
||||
PLUGIN_VERSION = None
|
||||
|
||||
def __init__(self):
|
||||
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
|
||||
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
|
||||
self.init_plugin()
|
||||
self.register_hooks()
|
||||
|
||||
def init_plugin(self):
|
||||
"""
|
||||
插件初始化逻辑
|
||||
子类可以重写此方法来实现特定的初始化操作
|
||||
"""
|
||||
logger.info(f'{self.PLUGIN_NAME} initialized.')
|
||||
|
||||
def register_hooks(self):
|
||||
"""
|
||||
注册插件钩子
|
||||
子类可以重写此方法来注册特定的钩子
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_plugin_info(self):
|
||||
"""
|
||||
获取插件信息
|
||||
:return: 包含插件元数据的字典
|
||||
"""
|
||||
return {
|
||||
'name': self.PLUGIN_NAME,
|
||||
'description': self.PLUGIN_DESCRIPTION,
|
||||
'version': self.PLUGIN_VERSION
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
ARTICLE_DETAIL_LOAD = 'article_detail_load'
|
||||
ARTICLE_CREATE = 'article_create'
|
||||
ARTICLE_UPDATE = 'article_update'
|
||||
ARTICLE_DELETE = 'article_delete'
|
||||
|
||||
ARTICLE_CONTENT_HOOK_NAME = "the_content"
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import os
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def load_plugins():
|
||||
"""
|
||||
Dynamically loads and initializes plugins from the 'plugins' directory.
|
||||
This function is intended to be called when the Django app registry is ready.
|
||||
"""
|
||||
for plugin_name in settings.ACTIVE_PLUGINS:
|
||||
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
|
||||
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
|
||||
try:
|
||||
__import__(f'plugins.{plugin_name}.plugin')
|
||||
logger.info(f"Successfully loaded plugin: {plugin_name}")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue