nch注释 #41

Merged
phm9gvnzi merged 2 commits from nch_branch into master 3 months ago

@ -8,40 +8,73 @@ from . import utils
from .models import BlogUser
# 登录表单继承自Django的AuthenticationForm
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
# 调用父类的初始化方法
super(LoginForm, self).__init__(*args, **kwargs)
# 为用户名字段添加占位符和CSS类
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
# 为密码字段添加占位符和CSS类
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
# 注册表单继承自Django的UserCreationForm
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
# 调用父类的初始化方法
super(RegisterForm, self).__init__(*args, **kwargs)
# 为各个表单字段添加占位符和CSS类
# 为用户名字段自定义widget
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
attrs={
'placeholder': "username", # 输入框内的提示文本,用户点击时会消失
"class": "form-control" # CSS类名用于应用Bootstrap等UI框架的样式
})
# 为邮箱字段自定义widget
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
attrs={
'placeholder': "email", # 提示用户输入邮箱地址
"class": "form-control" # 统一的表单控件样式类
})
# 为密码字段自定义widget密码输入框1
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
attrs={
'placeholder': "password", # 提示用户输入密码
"class": "form-control" # 密码输入框会显示为圆点或星号,隐藏实际内容
})
# 为确认密码字段自定义widget密码输入框2
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
attrs={
'placeholder': "repeat password", # 提示用户再次输入密码进行确认
"class": "form-control" # 同样使用密码输入框类型
})
# 邮箱验证方法
def clean_email(self):
email = self.cleaned_data['email']
# 检查邮箱是否已存在
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
# 元类配置
class Meta:
# 指定使用的用户模型
model = get_user_model()
# 表单包含的字段
fields = ("username", "email")
# 忘记密码表单
class ForgetPasswordForm(forms.Form):
# 新密码字段
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
@ -52,6 +85,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 确认密码字段
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
@ -62,6 +96,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 邮箱字段
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
@ -72,6 +107,7 @@ class ForgetPasswordForm(forms.Form):
),
)
# 验证码字段
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
@ -82,17 +118,21 @@ class ForgetPasswordForm(forms.Form):
),
)
# 确认密码验证方法
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
# 检查两次输入的密码是否一致
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
# 验证密码强度
password_validation.validate_password(password2)
return password2
# 邮箱验证方法
def clean_email(self):
user_email = self.cleaned_data.get("email")
# 检查邮箱是否存在
if not BlogUser.objects.filter(
email=user_email
).exists():
@ -100,8 +140,10 @@ class ForgetPasswordForm(forms.Form):
raise ValidationError(_("email does not exist"))
return user_email
# 验证码验证方法
def clean_code(self):
code = self.cleaned_data.get("code")
# 验证邮箱和验证码是否匹配
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
@ -111,7 +153,9 @@ class ForgetPasswordForm(forms.Form):
return code
# 忘记密码验证码请求表单
class ForgetPasswordCodeForm(forms.Form):
# 邮箱字段,用于发送验证码
email = forms.EmailField(
label=_('Email'),
)

@ -5,16 +5,41 @@ from django.utils.timezone import now
# Create your models here.
class OwnTrackLog(models.Model):
"""
OwnTracks位置数据记录模型
用于存储移动设备通过OwnTracks应用上报的地理位置信息
"""
# 设备标识符字段必填最大长度100字符
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
# 纬度坐标,浮点数类型
lat = models.FloatField(verbose_name='纬度')
# 经度坐标,浮点数类型
lon = models.FloatField(verbose_name='经度')
# 记录创建时间,自动设置为当前时间
creation_time = models.DateTimeField('创建时间', default=now)
def __str__(self):
"""
定义模型的字符串表示形式
在Admin后台和其他显示场合使用设备ID作为标识
"""
return self.tid
class Meta:
"""模型元数据配置"""
# 默认按创建时间升序排列
ordering = ['creation_time']
# 在Admin后台中显示的单数名称
verbose_name = "OwnTrackLogs"
# 在Admin后台中显示的复数名称
verbose_name_plural = verbose_name
get_latest_by = 'creation_time'
# 指定获取最新记录时使用的字段
get_latest_by = 'creation_time'

@ -9,56 +9,83 @@ from .models import OwnTrackLog
# Create your tests here.
class OwnTrackLogTest(TestCase):
"""测试OwnTrackLog位置追踪功能"""
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
"""测试初始化设置"""
self.client = Client() # Django测试客户端
self.factory = RequestFactory() # 请求工厂,用于创建请求对象
def test_own_track_log(self):
"""测试位置数据记录功能"""
# 测试用例1正常的位置数据提交
o = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
'tid': 12, # 设备ID
'lat': 123.123, # 纬度
'lon': 134.341 # 经度
}
# 发送POST请求提交位置数据
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
json.dumps(o), # 将数据转换为JSON格式
content_type='application/json' # 设置内容类型为JSON
)
# 验证数据是否成功保存到数据库
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
self.assertEqual(length, 1) # 应该有一条记录
# 测试用例2缺少经度的不完整数据提交
o = {
'tid': 12,
'lat': 123.123
# 缺少lon字段
}
# 发送不完整数据的POST请求
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
content_type='application/json'
)
# 验证不完整数据不应该被保存记录数仍为1
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
# 测试用例3未登录用户访问地图页面
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 302)
self.assertEqual(rsp.status_code, 302) # 应该重定向到登录页面
# 创建超级用户用于登录测试
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
# 使用新创建的用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建一条位置记录用于后续测试
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
# 测试用例4登录用户访问日期列表页面
rsp = self.client.get('/owntracks/show_dates')
self.assertEqual(rsp.status_code, 200)
self.assertEqual(rsp.status_code, 200) # 应该成功返回
# 测试用例5登录用户访问地图页面
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 200)
self.assertEqual(rsp.status_code, 200) # 应该成功返回
# 测试用例6登录用户获取位置数据默认日期
rsp = self.client.get('/owntracks/get_datas')
self.assertEqual(rsp.status_code, 200)
self.assertEqual(rsp.status_code, 200) # 应该成功返回
# 测试用例7登录用户获取指定日期的位置数据
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
self.assertEqual(rsp.status_code, 200)
self.assertEqual(rsp.status_code, 200) # 应该成功返回

@ -2,11 +2,32 @@ from django.urls import path
from . import views
# 定义应用命名空间用于URL反向解析时区分不同应用的同名URL
app_name = "owntracks"
# URL模式配置列表
urlpatterns = [
# 接收OwnTracks位置数据上报的端点
# 路径:/owntracks/logtracks
# 视图manage_owntrack_log - 处理位置数据存储
# 名称logtracks - 用于URL反向解析
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
# 显示轨迹地图页面的端点
# 路径:/owntracks/show_maps
# 视图show_maps - 渲染地图展示页面
# 名称show_maps - 用于URL反向解析
path('owntracks/show_maps', views.show_maps, name='show_maps'),
# 获取位置数据API端点
# 路径:/owntracks/get_datas
# 视图get_datas - 返回JSON格式的位置轨迹数据
# 名称get_datas - 用于URL反向解析
path('owntracks/get_datas', views.get_datas, name='get_datas'),
# 显示有记录的日期列表端点
# 路径:/owntracks/show_dates
# 视图show_log_dates - 显示所有有位置记录的日期
# 名称show_dates - 用于URL反向解析
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
]

@ -16,112 +16,181 @@ from django.views.decorators.csrf import csrf_exempt
from .models import OwnTrackLog
# 获取logger实例用于记录日志
logger = logging.getLogger(__name__)
# 处理OwnTracks位置数据的POST请求不需要CSRF验证
@csrf_exempt
def manage_owntrack_log(request):
"""
接收并存储OwnTracks移动端发送的位置数据
支持格式JSON格式的位置信息包含tid(设备ID)lat(纬度)lon(经度)
"""
try:
# 解析请求中的JSON数据
s = json.loads(request.read().decode('utf-8'))
tid = s['tid']
lat = s['lat']
lon = s['lon']
tid = s['tid'] # 设备标识符
lat = s['lat'] # 纬度
lon = s['lon'] # 经度
# 记录接收到的位置信息日志
logger.info(
'tid:{tid}.lat:{lat}.lon:{lon}'.format(
tid=tid, lat=lat, lon=lon))
# 验证必需字段是否存在
if tid and lat and lon:
# 创建新的位置记录对象
m = OwnTrackLog()
m.tid = tid
m.lat = lat
m.lon = lon
m.save()
return HttpResponse('ok')
m.save() # 保存到数据库
return HttpResponse('ok') # 返回成功响应
else:
return HttpResponse('data error')
return HttpResponse('data error') # 数据不完整错误
except Exception as e:
# 记录异常信息
logger.error(e)
return HttpResponse('error')
return HttpResponse('error') # 返回错误响应
# 显示地图页面,需要用户登录
@login_required
def show_maps(request):
"""
显示位置轨迹地图页面
仅超级用户可以访问支持按日期筛选显示轨迹
"""
if request.user.is_superuser:
# 设置默认日期为当前UTC日期
defaultdate = str(datetime.datetime.now(timezone.utc).date())
# 从GET参数获取日期如未提供则使用默认日期
date = request.GET.get('date', defaultdate)
context = {
'date': date
}
# 渲染地图显示页面
return render(request, 'owntracks/show_maps.html', context)
else:
# 非超级用户返回403禁止访问
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
# 显示有位置记录的日期列表,需要用户登录
@login_required
def show_log_dates(request):
"""
获取所有有位置记录的日期列表
用于在地图页面中选择查看特定日期的轨迹
"""
# 从数据库获取所有记录的时间戳
dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
# 提取日期部分,去重并排序
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
context = {
'results': results
'results': results # 日期列表
}
# 渲染日期列表页面
return render(request, 'owntracks/show_log_dates.html', context)
def convert_to_amap(locations):
"""
将GPS坐标批量转换为高德地图坐标
由于高德API限制每次最多转换30个坐标点
参数:
locations: OwnTrackLog对象的查询集
返回:
转换后的坐标字符串格式为"经度,纬度;经度,纬度;..."
"""
convert_result = []
# 创建迭代器用于分批处理
it = iter(locations)
# 每次处理30个坐标点
item = list(itertools.islice(it, 30))
while item:
# 将坐标格式化为高德API要求的格式
datas = ';'.join(
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
# 高德地图API密钥
key = '8440a376dfc9743d8924bf0ad141f28e'
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
query = {
'key': key,
'locations': datas,
'coordsys': 'gps'
'locations': datas, # 要转换的坐标
'coordsys': 'gps' # 源坐标系为GPS
}
# 调用高德坐标转换API
rsp = requests.get(url=api, params=query)
result = json.loads(rsp.text)
if "locations" in result:
convert_result.append(result['locations'])
item = list(itertools.islice(it, 30))
convert_result.append(result['locations']) # 添加转换结果
item = list(itertools.islice(it, 30)) # 获取下一批坐标
# 合并所有批次的转换结果
return ";".join(convert_result)
# 获取特定日期的位置数据,需要用户登录
@login_required
def get_datas(request):
"""
根据日期参数获取该日期所有设备的位置轨迹数据
返回JSON格式包含每个设备的轨迹路径
支持两种坐标格式
1. 原始GPS坐标当前使用
2. 高德转换坐标注释状态
"""
# 设置默认查询时间为当前日期的开始时间UTC
now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
querydate = django.utils.timezone.datetime(
now.year, now.month, now.day, 0, 0, 0)
# 如果提供了日期参数,解析并设置查询日期
if request.GET.get('date', None):
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
querydate = django.utils.timezone.datetime(
date[0], date[1], date[2], 0, 0, 0)
# 查询日期范围从查询日期的00:00到次日00:00
nextdate = querydate + datetime.timedelta(days=1)
# 获取该日期范围内的所有位置记录
models = OwnTrackLog.objects.filter(
creation_time__range=(querydate, nextdate))
result = list()
if models and len(models):
# 按设备ID分组处理
for tid, item in groupby(
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
d = dict()
d["name"] = tid
d["name"] = tid # 设备ID作为轨迹名称
paths = list()
# 使用高德转换后的经纬度
# 使用高德转换后的经纬度当前注释掉使用原始GPS坐标
# locations = convert_to_amap(
# sorted(item, key=lambda x: x.creation_time))
# for i in locations.split(';'):
# paths.append(i.split(','))
# 使用GPS原始经纬度
# 使用GPS原始经纬度按时间排序
for location in sorted(item, key=lambda x: x.creation_time):
paths.append([str(location.lon), str(location.lat)])
d["path"] = paths
result.append(d)
return JsonResponse(result, safe=False)
d["path"] = paths # 设备的轨迹路径
result.append(d) # 添加到结果列表
# 返回JSON格式的轨迹数据
return JsonResponse(result, safe=False)
Loading…
Cancel
Save