Dust8 的博客

读书百遍其义自见

0%

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def save_model(self, request, obj, form, change):
# form.initial 表单里面的初始值保存着原来的数据, cleaned_data 里面就是更改后的数据
# 新增获取不到,要给个默认值
user_info = form.initial.get("user_info",None)
try:
user_instance = obj.user_classmets
# 这是保存改动
super().save_model(request, obj, form, change)
# 特殊的目的,因为需要保存后才能处理
xxx(user_instance)
except Exception as ex:
# 这里在外键后面直接加_id,来设置数字,不然必须是实例
# 出错了还原数据
obj.user_info_id = user_info
obj.save()
# 只发送错误信息, 更改的信息不显示, 不然会叠加显示2条信息,导致错误信息被遮罩
messages.error(request, f"{ex}")
messages.set_level(request, messages.ERROR)

参考链接

DATA_UPLOAD_MAX_NUMBER_FIELDS 可以用来设置通过 GET 或 POST 接收到的参数的最大数量.默认是 1000.

后台管理页面有个 TabularInlineStackedInline ,里面的内容条数太多导致报错.可以通过在配置文件里面设置 DATA_UPLOAD_MAX_NUMBER_FIELDS 的值来解决, 例如设置为

1
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000*10

参考链接

django-filter 可以用来过滤查询. 跟 Django REST framework 很配.

使用

1
2
3
4
5
6
7
8
9
# 模型
from django.db import models

class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField()
description = models.TextField()
release_date = models.DateField()
manufacturer = models.ForeignKey(Manufacturer)
1
2
3
4
5
6
7
# 过滤器
import django_filters

class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['price', 'release_date']

指定字段, 排除字段

1
2
3
4
5
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
# 指定字段
fields = ['username', 'last_login']
1
2
3
4
5
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
# 排除字段
exclude = ['username', 'last_login']

过滤大于和小于或者包含等于

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ProductFilter(django_filters.FilterSet):
price = django_filters.NumberFilter()
price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt')
price__lt = django_filters.NumberFilter(field_name='price', lookup_expr='lt')
price__lte = django_filters.NumberFilter(field_name='price', lookup_expr='lte')

class Meta:
model = Product

class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'price': ['lt', 'gt'],
'release_date': ['exact', 'year__gt'],
}

过滤范围

一般是名字里面包含 Range, 例如 RangeFilter , NumericRangeFilter , DateRangeFilter , DateFromToRangeFilter, 查询时 url/?date_after=2016-01-01&date_before=2016-02-01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Comment(models.Model):
date = models.DateField()
time = models.TimeField()

class F(FilterSet):
date = DateFromToRangeFilter()

class Meta:
model = Comment
fields = ['date']

# Range: Comments added between 2016-01-01 and 2016-02-01
f = F({'date_after': '2016-01-01', 'date_before': '2016-02-01'})

# Min-Only: Comments added after 2016-01-01
f = F({'date_after': '2016-01-01'})

# Max-Only: Comments added before 2016-02-01
f = F({'date_before': '2016-02-01'})

自定义 method

自定义搜索多个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# https://django-filter.readthedocs.io/en/stable/guide/usage.html#customize-filtering-with-filter-method
class F(django_filters.FilterSet):
username = CharFilter(method='my_custom_filter')

class Meta:
model = User
fields = ['username']

def my_custom_filter(self, queryset, name, value):
return queryset.filter(**{
name: value,
})

# https://django-filter.readthedocs.io/en/stable/guide/tips.html#filtering-by-relative-times
from django.utils import timezone
from datetime import timedelta
...

class DataModel(models.Model):
time_stamp = models.DateTimeField()


class DataFilter(django_filters.FilterSet):
hours = django_filters.NumberFilter(
field_name='time_stamp', method='get_past_n_hours', label="Past n hours")

def get_past_n_hours(self, queryset, field_name, value):
time_threshold = timezone.now() - timedelta(hours=int(value))
return queryset.filter(time_stamp__gte=time_threshold)

class Meta:
model = DataModel
fields = ('hours',)
1
2
3
4
5
6
7
8
9
10
# 同时搜索多个字段
class ClassSellInfoFilter(filters_rest.FilterSet):
fuzzy_search = filters_rest.CharFilter(method='search_title_or_class_tag')

def search_title_or_class_tag(self, queryset, name, value):
return queryset.filter(Q(class_sell_title__icontains=value) | Q(class_name__class_tag__icontains=value))

class Meta:
model = ClassSellInfo
fields = ['fuzzy_search']

参考链接

django-import-export/django-import-exportdjango 后台管理页面导入导出插件.

安装

1
pip install django-import-export
1
2
3
4
5
# settings.py
INSTALLED_APPS = (
...
'import_export',
)

使用

创建导入导出资源类

drf 的序列化类很类似. 多看文档, 有各种需求的例子, 例如自定义字段, 字段排序, 关联字段等.

1
2
3
4
5
6
7
8
9
# app/admin.py

from import_export import resources
from core.models import Book

class BookResource(resources.ModelResource):

class Meta:
model = Book

和 admin 整合

这里要注意看它的 Admin 接口, 有很多种方式, 例如

  • ExportActionMixin
  • ExportActionModelAdmin
  • ExportMixin
  • ImportMixin

看名字就知道, 其实就是把导入,导出各种组合. Mixinadmin.ModelAdmin 组合变成 ModelAdmin.
含有 Action 就是直接在列表页显示导出选项, 不需要进入下一层页面选择.

1
2
3
4
5
6
7
from .models import Book
from import_export.admin import ImportExportModelAdmin

class BookAdmin(ImportExportModelAdmin):
resource_class = BookResource

admin.site.register(Book, BookAdmin)

常见使用

1
2
3
4
5
6
# 定义导出字段 fields
class BookResource(resources.ModelResource):

class Meta:
model = Book
fields = ('id', 'name', 'price', 'author__name', )
1
2
3
4
5
6
# 排除导出字段 exclude
class BookResource(resources.ModelResource):

class Meta:
model = Book
exclude = ('imported', )
1
2
3
4
5
6
7
# 排序导出字段 export_order
class BookResource(resources.ModelResource):

class Meta:
model = Book
fields = ('id', 'name', 'author', 'price',)
export_order = ('id', 'price', 'author', 'name')

导出自定义字段

命名方式 dehydrate_<fieldname>

1
2
3
4
5
6
7
8
9
10
from import_export.fields import Field

class BookResource(resources.ModelResource):
full_title = Field()

class Meta:
model = Book

def dehydrate_full_title(self, book):
return '%s by %s' % (book.name, book.author.name)

问题

1. 如果有自定义的 actions, 导入导出会不显示

查看 django-import-export 源码

1
2
3
4
5
6
7
8
9
10
11
# import_exprt/admin.py
class ExportActionMixin(ExportMixin):
...

actions = admin.ModelAdmin.actions + [export_admin_action]

class ExportActionModelAdmin(ExportActionMixin, admin.ModelAdmin):
"""
Subclass of ModelAdmin with export functionality implemented as an
admin action.
"""

看起来是没问题, 实际上这个 actions 已经被我们自定义的 actions 覆盖了. 在加上就可以了.

1
2
3
4
5
class BookAdmin(ImportExportModelAdmin):
resource_class = BookResource
actions= ["xxxx", ExportActionMixin.export_admin_action]

admin.site.register(Book, BookAdmin)

2.导出变成了导出全部, 多选功能没有用

目前只能用 ExportActionMixin 这类带有 Action 的方法, 让它在列表页直接选择, 不要进入下一层就可以实现多选导出.

参考链接

原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间
公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS)    

现有的 qps 是可以测试出来, 每天秒数是已知的, 那么能支撑的总 pv 数是可以算出来的.
现有 pv / 现有用户数 = 总的 pv / 总的用户数, 这样就可以算出能支撑的最大用户数.

计算现有 qps

Locust 是一个压测工具. 用 python 写的.
下面代码是测试一个接口, 在该文件目录运行 locust 就启动了, 在打开 http://localhost:8089 设置模拟用户数, 请求速度和请求地址.

1
2
3
4
5
6
7
8
9
10
11
# locustfile.py
import time
from locust import HttpUser, task


class QuickstartUser(HttpUser):
@task
def hello_world(self):
self.client.get(
"/classsellinfo/?page=1&page_size=10&ordering=-created&class_price__gt=0.1"
)

在 4 核 16G 的电脑上用 wsl2 + gunicorn 8(建议是(2*CPU)+1) 个 worker 上大概是 150 rps. 这个接口访问了本地 mysql, 并且无缓存设置.

统计现有pv

GoAccess 是一个开源的实时 web 日志分析器, 支持命令行和浏览器交互.

  • 分析用户的使用情况
  • 统计当前用户的 pv 来预估后期更多用户的 pv
  • 查看请求多的接口可以优先优化

分析 nginx 单个日志文件

分析完后打开 report.html 就可以看到统计情况

1
goaccess access.log -o report.html --log-format=COMBINED

分析 nginx 多个压缩日志文件

1
zcat -f access.log* | goaccess -a -o report.html --log-format=COMBINED

参考链接