Compare commits

...

25 Commits

Author SHA1 Message Date
Ching
306e54bcd5 feat(recipe edit): 修改删除菜谱按钮位置
修改删除菜谱按钮位置

Signed-off-by: Ching <loooching@gmail.com>
2022-02-12 15:52:16 +08:00
Ching
aa2f401636 fix(scripts): fix typo
fix typo

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 22:25:47 +08:00
Ching
f44ced4946 feat(scripts): 修复可能导致重复发送图片的问题;修改 logger 位置
修复可能导致重复发送图片的问题;修改 logger 位置

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 22:13:17 +08:00
Ching
1cc841f3cc feat(scripts): 增加 Instagram 同步至 mastodon 脚本
增加 Instagram 同步至 mastodon 脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 21:47:47 +08:00
Ching
cc0c22c68b Merge branch 'develop' of github.com:looching/dsite into develop 2022-02-11 19:54:28 +08:00
Ching
b4b5a0e68a feat(scripts): 增加 instagram 同步 mastodon 脚本
增加 instagram 同步 mastodon 脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 19:53:35 +08:00
Ching
e7a579e168 fix(recipe models): 修复通过嘟嘟创建菜谱时没有设置 status 导致创建失败的问题
修复通过嘟嘟创建菜谱时没有设置 status 导致创建失败的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 22:38:12 +08:00
Ching
b26000205b fix(scripts): 修复 bitwarden 备份数据路径有误的问题
修复 bitwarden 备份数据路径有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 22:35:57 +08:00
Ching
1bea995dc1 feat(scripts): 增加将图片转为灰度图的脚本
增加将图片转为灰度图的脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 15:30:30 +08:00
Ching
844ce1bfd5 feat(scripts): 增加 dsite 部署脚本;修复嘟嘟机部署提示有误的问题
增加 dsite 部署脚本;修复嘟嘟机部署提示有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 02:23:07 +08:00
Ching
83a3bd193b feat(recipe detail page): 菜谱编辑页增加删除按钮
菜谱编辑页增加删除按钮

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 01:41:03 +08:00
Ching
abce5b18fd feat(recipe api): 增加删除菜谱接口
增加删除菜谱接口

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 00:38:30 +08:00
Ching
1277accf5c feat(scripts): 增加bitwarden备份脚本
增加bitwarden备份脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-04 16:49:23 +08:00
Ching
9b115417d3 feat(recipe mobile): 修改菜谱后弹出成功提示
修改菜谱后弹出成功提示

Signed-off-by: Ching <loooching@gmail.com>
2022-02-03 01:21:24 +08:00
Ching
219e7be4a6 feat(scripts): 增加重启dodo脚本
增加重启dodo脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-03 00:49:49 +08:00
Ching
86fdc14b56 fix(dodo): 修复 dodo 部署成功消息没发送的问题
修复 dodo 部署成功消息没发送的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-23 23:38:50 +08:00
Ching
effc93b56d feat(dodo): 增加飞书 dodo 部署逻辑
增加飞书 dodo 部署逻辑

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 21:34:31 +08:00
Ching
f683341f66 style(*.py): use Black as python code formatter
use Black as python code formatter

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 18:02:32 +08:00
Ching
beb10e00a9 feat(recipe): 修改菜谱飞书卡片样式
修改菜谱飞书卡片样式

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 00:01:44 +08:00
Ching
a0fa3ac98c fix(dodo): 修复指令执行后当成嘟嘟发送的问题
修复指令执行后当成嘟嘟发送的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 14:22:22 +08:00
Ching
e51ab984fd fix(dodo): 修复指令识别有误的问题
修复指令识别有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:54:46 +08:00
Ching
f3c309e38c fix(dodo): fix import error
fix import error

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:39:08 +08:00
Ching
c8d5195669 Merge branch 'develop' of git.tunpok.com:ching/dsite into develop 2022-01-21 00:14:39 +08:00
Ching
ea42cef954 feat(dodo): 增加创建菜谱功能
增加创建菜谱功能

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:14:30 +08:00
Ching
33631e2b94 fix(dodo): 修复删除嘟文时报错的问题
修复删除嘟文时报错的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-19 09:35:45 +08:00
31 changed files with 1026 additions and 534 deletions

13
.vscode/settings.json vendored
View File

@ -1,2 +1,15 @@
{
"python.formatting.provider": "black",
"python.formatting.blackArgs": [
"-S",
"-C",
"-l 120"
],
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.rulers": [
120
],
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": "active"
}

0
a.txt
View File

View File

@ -2,6 +2,7 @@
from rest_framework import pagination
from rest_framework.response import Response
class PagePaginationWithPageCount(pagination.PageNumberPagination):
page_size_query_param = 'page_size'
@ -9,5 +10,3 @@ class PagePaginationWithPageCount(pagination.PageNumberPagination):
response = super().get_paginated_response(data)
response.data['page_count'] = self.page.paginator.num_pages
return response

View File

@ -31,3 +31,5 @@ user-agents==2.2.0
wcwidth==0.2.5
zipp==3.5.0
redis==4.1.0
black
instagram_private_api

View File

@ -37,14 +37,11 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# third party
'corsheaders',
'rest_framework',
'rest_framework.authtoken',
'django_filters',
# user apps
'timer',
'recipe',
@ -74,9 +71,9 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
]
},
}
]
WSGI_APPLICATION = 'dsite.wsgi.application'
@ -85,30 +82,17 @@ WSGI_APPLICATION = 'dsite.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3')}}
# Password validation
# https://docs.djangoproject.com/en/1.11/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',
},
{'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'},
]
@ -140,11 +124,10 @@ REST_FRAMEWORK = {
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly'
# ],
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PAGINATION_CLASS': 'core.pagination.PagePaginationWithPageCount',
'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}
# CORS
@ -159,3 +142,10 @@ CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
LARK_WEBHOOK_URL = ''
LARK_WEBHOOK_SECRET = ''
MASTODON_SYNCED_IMAGES_LOG = ''
IG_PRIVATE_API_SETTINGS = ''
IG_LOGIN_USERNAME = ''
IG_LOGIN_PASSWORD = ''
MASTODON_NOFAN_ACCESS_TOKEN = ''

View File

@ -1,5 +1,5 @@
<template>
<el-row justify="left">
<el-row justify="left" gutter="10">
<el-col>
<el-form :rules="rules" ref="form" :model="form" label-position="left">
<el-form-item label="名字" prop="name">
@ -54,6 +54,16 @@
<el-input type="textarea" v-model="form.note"></el-input>
</el-form-item>
<el-form-item>
<el-col :span="8" v-if="recipe_id">
<el-button
type="danger"
plain
class="summit-recipe"
@click="onSubmitDelete(recipe_id)"
>删除</el-button
>
</el-col>
<el-col :span="16" v-if="recipe_id">
<el-button
type="primary"
plain
@ -61,6 +71,16 @@
@click="onSubmit(recipe_id)"
>提交</el-button
>
</el-col>
<el-col :span="24" v-else>
<el-button
type="primary"
plain
class="summit-recipe"
@click="onSubmit(recipe_id)"
>提交</el-button
>
</el-col>
</el-form-item>
</el-form>
</el-col>
@ -69,6 +89,8 @@
<script>
import axios from 'axios';
import config from '@/config/index';
import { ElMessage } from 'element-plus';
export default {
props: ['recipe_'],
watch: {
@ -105,6 +127,10 @@ export default {
axios
.post(config.publicPath + '/recipe/recipe/', data)
.then(function () {
ElMessage({
message: '创建成功.',
type: 'success',
});
location.reload();
})
.catch(function (error) {
@ -114,13 +140,27 @@ export default {
axios
.put(config.publicPath + '/recipe/recipe/' + recipe_id, data)
.then(function () {
location.reload();
ElMessage({
message: '修改成功.',
type: 'success',
});
})
.catch(function (error) {
console.log(error);
});
}
},
onSubmitDelete(recipe_id) {
axios
.delete(config.publicPath + '/recipe/recipe/' + recipe_id)
.then(function () {
ElMessage.error('删除成功.');
location.reload();
})
.catch(function (error) {
console.log(error);
});
},
},
};
</script>
@ -128,5 +168,6 @@ export default {
<style scoped>
.summit-recipe {
width: 100%;
margin-bottom: 10px;
}
</style>

View File

@ -43,10 +43,25 @@
/>
</van-cell-group>
<div class="recipe-create">
<van-row gutter="20">
<van-col span="8" v-if="recipe_id">
<van-button
class="submit-button"
round
type="danger"
plain
hairline
:disabled="disable_submit"
@click="onSubmitDelete(recipe_id)"
:loading="loading"
>删除</van-button
>
</van-col>
<van-col span="16" v-if="recipe_id">
<van-button
class="submit-button"
round
type="primary"
block
plain
hairline
:disabled="disable_submit"
@ -54,12 +69,38 @@
:loading="loading"
>提交</van-button
>
</van-col>
<van-col span="24" v-else>
<van-button
class="submit-button"
round
type="primary"
plain
hairline
:disabled="disable_submit"
@click="onSubmit(recipe_id)"
:loading="loading"
>提交</van-button
>
</van-col>
</van-row>
</div>
</van-form>
</template>
<script>
import { Form, Field, CellGroup, Radio, RadioGroup, Rate, Button } from 'vant';
import {
Form,
Field,
CellGroup,
Radio,
RadioGroup,
Rate,
Button,
Toast,
Col,
Row,
} from 'vant';
import axios from 'axios';
import config from '@/config/index';
import router from '@/router/index';
@ -81,6 +122,8 @@ export default {
[RadioGroup.name]: RadioGroup,
[Rate.name]: Rate,
[Button.name]: Button,
[Col.name]: Col,
[Row.name]: Row,
},
data() {
return {
@ -118,11 +161,30 @@ export default {
(response) => (response, router.push({ name: 'RecipeMobileHome' }))
);
} else {
axios
.put(config.publicPath + '/recipe/recipe/' + recipe_id, data)
.then((this.loading = false));
axios.put(config.publicPath + '/recipe/recipe/' + recipe_id, data).then(
(Toast.success({
message: '修改成功',
forbidClick: true,
duration: 500,
}),
(this.loading = false))
);
}
},
onSubmitDelete(recipe_id) {
if (!this.form.name) {
return;
}
this.loading = true;
axios.delete(config.publicPath + '/recipe/recipe/' + recipe_id).then(
(Toast.success({
message: '删除成功',
forbidClick: true,
duration: 500,
}),
(this.loading = false))
);
},
},
};
</script>
@ -130,4 +192,7 @@ export default {
.recipe-create {
margin: 20px 16px;
}
.submit-button {
width: 100%;
}
</style>

View File

@ -7,8 +7,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
]
dependencies = []
operations = [
migrations.CreateModel(
@ -21,5 +20,5 @@ class Migration(migrations.Migration):
('rate', models.IntegerField(default=0)),
('difficulty', models.IntegerField(default=0)),
],
),
)
]

View File

@ -5,15 +5,11 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0001_initial'),
]
dependencies = [('recipe', '0001_initial')]
operations = [
migrations.AlterField(
model_name='recipe',
name='recipe_type',
field=models.CharField(default='meat', max_length=32),
model_name='recipe', name='recipe_type', field=models.CharField(default='meat', max_length=32)
),
migrations.CreateModel(
name='DailyRecipe',

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2022-02-04 16:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0002_auto_20211002_1926'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='status',
field=models.CharField(default='active', max_length=32),
),
]

View File

@ -6,37 +6,115 @@ from utils import const
import utils
class Recipe(models.Model):
name = models.CharField(max_length=128)
recipe_type = models.CharField(max_length=32,
default=const.RECIPE_TYPE_MEAT)
recipe_type = models.CharField(max_length=32, default=const.RECIPE_TYPE_MEAT)
status = models.CharField(max_length=32, default=const.RECIPE_STATUS_ACTIVE)
note = models.TextField(null=True)
rate = models.IntegerField(default=0)
difficulty = models.IntegerField(default=0)
def serialize(self, verbose=False):
data = {
'id': self.id,
'name': self.name,
'recipe_type': self.recipe_type,
}
data = {'id': self.id, 'name': self.name, 'recipe_type': self.recipe_type}
if verbose:
data.update({
'difficulty': self.difficulty,
'rate': self.rate,
'note': self.note,
})
data.update({'difficulty': self.difficulty, 'rate': self.rate, 'note': self.note})
return data
@classmethod
def create_from_str(cls, content):
content = content.strip()
if not content:
return
name, *data = content.split(' ')
recipe_type = rate = difficulty = None
keys = [recipe_type, rate, difficulty]
for x in range(len(data)):
keys[x] = data[x]
recipe_type, rate, difficulty = keys
if recipe_type == '':
recipe_type = const.RECIPE_TYPE_MEAT
elif recipe_type == '':
recipe_type = const.RECIPE_TYPE_VEGETABLE
elif recipe_type == '':
recipe_type = const.RECIPE_TYPE_SOUP
else:
recipe_type = const.RECIPE_TYPE_MEAT
if rate:
try:
rate = int(rate)
except:
rate = 0
else:
rate = 0
if difficulty:
try:
difficulty = int(difficulty)
except:
difficulty = 0
else:
difficulty = 0
recipe = cls.objects.create(
name=name, recipe_type=recipe_type, rate=rate, difficulty=difficulty, status=const.RECIPE_STATUS_ACTIVE
)
return recipe
@property
def display_recipe_type(self):
if self.recipe_type == const.RECIPE_TYPE_VEGETABLE:
return ''
elif self.recipe_type == const.RECIPE_TYPE_MEAT:
return ''
elif self.recipe_type == const.RECIPE_TYPE_SOUP:
return ''
return ''
@property
def display_rate(self):
return '🍚' * self.rate
@property
def display_difficult(self):
return '⭐️' * self.difficulty
def construct_lart_card(self):
data = {
"config": {"wide_screen_mode": True},
"header": {"title": {"tag": "plain_text", "content": self.name}, "template": "blue"},
"elements": [
{
"tag": "markdown",
"content": "**类型**%s\n**评分**%s\n**难度**%s"
% (self.display_recipe_type, self.display_rate, self.display_difficult),
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"tag": "plain_text", "content": "查看"},
"multi_url": {
"url": "https://recipe.tunpok.com/recipe/%s" % self.id,
"android_url": "https://recipe.tunpok.com/recipe-mobile/recipe/%s" % self.id,
"ios_url": "https://recipe.tunpok.com/recipe-mobile/recipe/%s" % self.id,
"pc_url": "https://recipe.tunpok.com/recipe/%s" % self.id,
},
"type": "primary",
},
{"tag": "button", "text": {"tag": "plain_text", "content": "删除"}, "type": "danger"},
],
},
],
}
return data
class DailyRecipe(models.Model):
recipes = models.ManyToManyField(Recipe)
date = models.DateField()
meal_type = models.CharField(
max_length=32,
default=const.DAILY_RECIPE_MEAL_TYPE_SUPPER,)
meal_type = models.CharField(max_length=32, default=const.DAILY_RECIPE_MEAL_TYPE_SUPPER)
def generate_recipe(self, prev_recipes=None, ignore_prev=False):
if not prev_recipes:
@ -50,9 +128,12 @@ class DailyRecipe(models.Model):
# meat
for x in range(0, 2):
while True:
recipe = Recipe.objects.filter(
recipe_type=const.RECIPE_TYPE_MEAT,
).order_by('?').first()
recipe = (
Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_MEAT)
.exclude(status=const.RECIPE_STATUS_DELETED)
.order_by('?')
.first()
)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
recipes.append(recipe.id)
break
@ -65,9 +146,12 @@ class DailyRecipe(models.Model):
# vegetable
for x in range(0, 1):
while True:
recipe = Recipe.objects.filter(
recipe_type=const.RECIPE_TYPE_VEGETABLE,
).order_by('?').first()
recipe = (
Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_VEGETABLE)
.exclude(status=const.RECIPE_STATUS_DELETED)
.order_by('?')
.first()
)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
recipes.append(recipe.id)
break
@ -79,9 +163,12 @@ class DailyRecipe(models.Model):
# soup
if random.randint(0, 2):
recipe = Recipe.objects.filter(
recipe_type=const.RECIPE_TYPE_SOUP,
).order_by('?').first()
recipe = (
Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_SOUP)
.exclude(status=const.RECIPE_STATUS_DELETED)
.order_by('?')
.first()
)
# if recipe not in recipes and recipe not in prev_recipes:
if recipe:
recipes.append(recipe.id)
@ -89,19 +176,13 @@ class DailyRecipe(models.Model):
self.recipes.set(Recipe.objects.filter(id__in=recipes))
def serialize(self):
data = {
const.RECIPE_TYPE_MEAT: [],
const.RECIPE_TYPE_VEGETABLE: [],
const.RECIPE_TYPE_SOUP: [],
}
data = {const.RECIPE_TYPE_MEAT: [], const.RECIPE_TYPE_VEGETABLE: [], const.RECIPE_TYPE_SOUP: []}
for key_, value_ in data.items():
for recipe in self.recipes.filter(
recipe_type=key_).order_by('id'):
for recipe in self.recipes.filter(recipe_type=key_).order_by('id'):
value_.append(recipe.serialize())
date = now()
date = date.replace(year=self.date.year, month=self.date.month,
day=self.date.day, hour=0, minute=0)
date = date.replace(year=self.date.year, month=self.date.month, day=self.date.day, hour=0, minute=0)
data['date'] = utils.timestamp_of(utils.day_start(date))
data['id'] = self.id
return data
@ -111,10 +192,7 @@ class DailyRecipe(models.Model):
today = localtime()
week_start = (today - datetime.timedelta(days=today.weekday())).date()
week_end = week_start + datetime.timedelta(days=6)
daily_recipes = cls.objects.filter(
date__gte=week_start,
date__lte=week_end,
).order_by('date')
daily_recipes = cls.objects.filter(date__gte=week_start, date__lte=week_end).order_by('date')
data = [{}] * (7 - len(daily_recipes))
for daily_recipe in daily_recipes:

View File

@ -4,15 +4,16 @@ from rest_framework import serializers
import recipe.models
class RecipeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
class Meta:
model = recipe.models.Recipe
fields = '__all__'
class WeekRecipeSerializer(serializers.ModelSerializer):
class Meta:
model = recipe.models.DailyRecipe
fields = '__all__'
@ -21,6 +22,7 @@ class WeekRecipeSerializer(serializers.ModelSerializer):
class DailyRecipeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True)
recipes = RecipeSerializer(many=True)
class Meta:
model = recipe.models.DailyRecipe
fields = '__all__'

View File

@ -1,4 +1,5 @@
from django.conf.urls import include, url
# from django.core.urlresolvers import reverse
from django.urls import path
from rest_framework import routers
@ -11,6 +12,5 @@ urlpatterns = [
url(r'^recipe/(?P<pk>\d+)$', views.RecipeAPI.as_view(), name='recipe-detail'),
url(r'^recipe/$', views.RecipeListAPI.as_view()),
url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()),
url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(),
name='dailyrecipe-detail'),
url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(), name='dailyrecipe-detail'),
]

View File

@ -11,7 +11,8 @@ import recipe.models
import recipe.serializers
from utils import const
class RecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView):
# authentication_classes = (authentication.TokenAuthentication,
# authentication.SessionAuthentication,
@ -20,15 +21,18 @@ class RecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
queryset = recipe.models.Recipe.objects.all()
serializer_class = recipe.serializers.RecipeSerializer
def perform_destroy(self, instance):
instance.status = const.RECIPE_STATUS_DELETED
instance.save(update_fields=['status'])
class RecipeListAPI(rest_framework.generics.ListAPIView,
rest_framework.generics.CreateAPIView):
class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
# authentication_classes = (authentication.TokenAuthentication,
# authentication.SessionAuthentication,
# authentication.BasicAuthentication)
# permission_classes = (permissions.IsAuthenticated,)
queryset = recipe.models.Recipe.objects.all()
queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED)
serializer_class = recipe.serializers.RecipeSerializer
filterset_fields = {
'recipe_type': const.FILTER_EXACT,
@ -37,8 +41,7 @@ class RecipeListAPI(rest_framework.generics.ListAPIView,
}
class WeekRecipeListAPI(rest_framework.generics.ListAPIView,
rest_framework.generics.CreateAPIView):
class WeekRecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
queryset = recipe.models.DailyRecipe.objects.all()
serializer_class = recipe.serializers.WeekRecipeSerializer
@ -48,13 +51,10 @@ class WeekRecipeListAPI(rest_framework.generics.ListAPIView,
today = localtime()
recipes = []
for x in range(0, 7 - today.weekday()):
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(
date=today + datetime.timedelta(days=x)
)
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(date=today + datetime.timedelta(days=x))
daily_recipe.generate_recipe(recipes)
recipes.extend(daily_recipe.recipes.values_list('id', flat=True))
return Response(recipe.models.DailyRecipe.get_week_recipe_data(),
status=status.HTTP_201_CREATED, headers={})
return Response(recipe.models.DailyRecipe.get_week_recipe_data(), status=status.HTTP_201_CREATED, headers={})
def get(self, request, *args, **kwargs):
data = recipe.models.DailyRecipe.get_week_recipe_data()
@ -69,15 +69,12 @@ class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
def post(self, request, *args, **kwargs):
daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk'])
daily_recipe.generate_recipe()
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED,
headers={})
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})
def put(self, request, *args, **kwargs):
daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk'])
recipes = request.data.get('meat', [])
recipes.extend(request.data.get('vegetable', []))
recipes.extend(request.data.get('soup', []))
daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(
id__in=recipes))
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED,
headers={})
daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(id__in=recipes))
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})

View File

@ -0,0 +1,83 @@
# coding: utf-8
from datetime import datetime
from dateutil import tz
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import os, tarfile
import smtplib
to_zone = tz.gettz('Asia/Shanghai')
localtime = datetime.now(tz=to_zone)
def make_targz(output_filename, source_dir):
"""
一次性打包目录为tar.gz
:param output_filename: 压缩文件名
:param source_dir: 需要打包的目录
:return:
"""
try:
with tarfile.open(output_filename, "w:gz") as tar:
tar.add(source_dir, arcname=os.path.basename(source_dir))
return True
except Exception as e:
print(e)
return False
def send_email(file_name):
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
# server.set_debuglevel(1)
from_addr = ''
to_addr = ''
to_zone = tz.gettz('Asia/Shanghai')
localtime = datetime.now(tz=to_zone)
msg = MIMEMultipart()
msg['From'] = _format_addr('Vaultwarden <%s>' % from_addr)
msg['To'] = _format_addr('<%s>' % to_addr)
msg['Subject'] = Header('bitwarden 备份 %s' % localtime.strftime('%Y-%m-%d'), 'utf-8').encode()
# 邮件正文是MIMEText:
msg.attach(MIMEText('backup file attached', 'plain', 'utf-8'))
# 添加附件就是加上一个MIMEBase从本地读取一个图片:
with open(file_name, 'rb') as f:
# 设置附件的MIME和文件名
mime = MIMEBase('tar.gz', 'tar.gz', filename=file_name)
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename=file_name)
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)
server.login(from_email, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
if __name__ == '__main__':
file_name = '/root/develop/vaultwarden/backup/bitwarden-%s.tar.gz' % localtime.strftime('%Y-%m-%d')
if make_targz(file_name, '/root/develop/vaultwarden/vw-data'):
send_email(file_name)

View File

@ -1,5 +1,14 @@
#!/usr/bin/env python
# --coding:utf-8--
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
@ -15,6 +24,9 @@ import time
import subprocess
from bs4 import BeautifulSoup
import recipe.models
from utils import const
APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3'
APP_ID = 'cli_a115fe8b83f9100c'
APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u'
@ -26,48 +38,46 @@ KEDAI_ID = '107263380636355825'
logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO)
logger = logging.getLogger('/root/develop/log/dodo.log')
mastodon = Mastodon(
access_token = 'Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE',
api_base_url = 'https://nofan.xyz'
)
mastodon_cli = Mastodon(access_token='Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE', api_base_url='https://nofan.xyz')
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
redis_cli = redis.Redis(host='localhost', port=6379, decode_responses=True)
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
@staticmethod
def str_to_bytes(data):
u_type = type(b"".decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data
@staticmethod
def _unpad(s):
return s[: -ord(s[len(s) - 1 :])]
def decrypt(self, enc):
iv = enc[: AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size :]))
def decrypt_string(self, enc):
enc = base64.b64decode(enc)
return self.decrypt(enc).decode('utf8')
def get_tenant_access_token(): # 获取token
token = redis_cli.get('tenant_access_token_%s' % APP_ID)
if token:
return token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
headers = {
"Content-Type": "application/json"
}
req_body = {
"app_id": APP_ID,
"app_secret": APP_SECRET
}
headers = {"Content-Type": "application/json"}
req_body = {"app_id": APP_ID, "app_secret": APP_SECRET}
data = bytes(json.dumps(req_body), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
@ -83,29 +93,24 @@ def get_tenant_access_token(): # 获取token
if code != 0:
logger.error("get tenant_access_token error, code =%s", code)
return ""
token = redis_cli.set('tenant_access_token_%s' % APP_ID,
rsp_dict.get("tenant_access_token", ""),
ex=60*30)
token = rsp_dict.get("tenant_access_token", "")
redis_cli.set('tenant_access_token_%s' % APP_ID, rsp_dict.get("tenant_access_token", ""), ex=60 * 30)
return token
def get_group_name(chat_id):
group_name = redis_cli.get('group_name_%s' % chat_id)
if not group_name:
url = "https://open.feishu.cn/open-apis/im/v1/chats/"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + get_tenant_access_token()
}
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + get_tenant_access_token()}
try:
resp = requests.get(url + chat_id, headers=headers)
resp_data = resp.json()
code = resp_data.get("code", -1)
if code == 0:
group_name = resp_data.get('data', {}).get('name')
redis_cli.set('group_name_%s' % chat_id,
group_name,
ex=60*60*24)
redis_cli.set('group_name_%s' % chat_id, group_name, ex=60 * 60 * 24)
except:
# todo: log
return
@ -113,6 +118,10 @@ def get_group_name(chat_id):
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
"""Serve a GET request."""
self.response("")
def do_POST(self):
# 解析请求 body
req_body = self.rfile.read(int(self.headers['content-length']))
@ -180,30 +189,32 @@ class RequestHandler(BaseHTTPRequestHandler):
group_name = get_group_name(msg.get("chat_id"))
text = '%s #%s' % (text, group_name)
else:
open_id = {"open_id": event.get("sender", {}).get(
'sender_id', {}).get('open_id')}
open_id = {"open_id": event.get("sender", {}).get('sender_id', {}).get('open_id')}
self.response("")
if orig_text.startswith('/'):
redis_cli.set(event_id, int(time.time()), ex=60 * 60 * 7)
if orig_text not in ['/last', '/del']:
if not orig_text.startswith('/deploy '):
flag = False
for action_ in ['/deploy ', '/菜谱 ']:
if orig_text.startswith(action_):
flag = True
break
if not flag:
self.msg_compoment(access_token, open_id, '指令错误')
return
if orig_text == '/last':
try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1)
statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id,
s_text.get_text(''))
self.msg_compoment(access_token, open_id, s_text.get_text(''))
except Exception as exc:
logger.error('operation error: %s', str(exc))
elif orig_text == '/del':
try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1)
Mastodon.status_delete(statuses[0]['id'])
statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
mastodon_cli.status_delete(statuses[0]['id'])
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id,
'已删除: ' + s_text.get_text(''))
self.msg_compoment(access_token, open_id, '已删除: ' + s_text.get_text(''))
except Exception as exc:
logger.error('operation error: %s', str(exc))
elif orig_text.startswith('/deploy '):
@ -213,19 +224,42 @@ class RequestHandler(BaseHTTPRequestHandler):
subprocess.call("/root/deploy/dsite_prepare.sh")
subprocess.run(["supervisorctl", "restart", "dsite"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
elif site_ == 'dodo':
self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_)
subprocess.run(["git", "pull"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
subprocess.run(["supervisorctl", "restart", "dodo"])
else:
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_)
elif orig_text.startswith('/菜谱 '):
content = orig_text.split('/菜谱 ')[1]
recipe_ = recipe.models.Recipe.create_from_str(content)
if recipe_:
self.msg_compoment(
access_token,
open_id,
None,
const.LARK_WEBHOOK_MSG_TYPE_INTERACTIVE,
recipe_.construct_lart_card(),
)
else:
self.msg_compoment(access_token, open_id, '⚠️ 创建失败 ⚠️')
return
try:
toot_resp = mastodon.status_post(text)
toot_resp = mastodon_cli.status_post(text)
if toot_resp.get('id'):
self.msg_compoment(access_token, open_id, '📟 dodo 📟')
redis_cli.set(event_id, int(time.time()), ex=60 * 60 * 7)
else:
self.msg_compoment(access_token, open_id, """⚠️ didi ⚠️
self.msg_compoment(
access_token,
open_id,
"""⚠️ didi ⚠️
%s
""" % json.loads(toot_resp))
"""
% json.loads(toot_resp),
)
except Exception as exc:
logger.error('send toot error: %s', str(exc))
@ -240,18 +274,17 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body.encode())
def send_message(self, token, open_id, text):
def send_message(self, token, open_id, text, msg_type=None, content=None):
url = "https://open.feishu.cn/open-apis/message/v4/send/"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
}
req_body = {
"msg_type": "text",
"content": {
"text": text
}
}
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + token}
if not msg_type:
msg_type = const.LARK_WEBHOOK_MSG_TYPE_TEXT
req_body = {"msg_type": msg_type}
if msg_type == const.LARK_WEBHOOK_MSG_TYPE_TEXT:
req_body['content'] = {'text': text}
elif msg_type == const.LARK_WEBHOOK_MSG_TYPE_INTERACTIVE:
req_body['card'] = content
req_body = dict(req_body, **open_id) # 根据open_id判断返回域
data = bytes(json.dumps(req_body), encoding='utf8')
@ -266,12 +299,11 @@ class RequestHandler(BaseHTTPRequestHandler):
rsp_dict = json.loads(rsp_body)
code = rsp_dict.get("code", -1)
if code != 0:
logger.error("send message error, code = %s, msg =%s",
code,
rsp_dict.get("msg", ""))
logger.error("send message error, code = %s, msg =%s", code, rsp_dict.get("msg", ""))
def msg_compoment(self, token, open_id, text, msg_type=None, content=None):
self.send_message(token, open_id, text, msg_type, content)
def msg_compoment(self, token, open_id, text):
self.send_message(token, open_id, text)
def run():
port = 5000

5
scripts/dsite_prepare.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
/root/.pyenv/versions/py37/bin/pip install -r /root/deploy/dsite/develop_requirements.txt
/root/.pyenv/versions/py37/bin/python /root/deploy/dsite/manage.py collectstatic --noinput
/root/.pyenv/versions/py37/bin/python /root/deploy/dsite/manage.py migrate

50
scripts/grayscale.py Normal file
View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import sys
import numpy as np
import cv2 # opencv-python
# 引入Python的可视化工具包 matplotlib
from matplotlib import pyplot as plt
def print_img_info(img):
print("================打印一下图像的属性================")
print("图像对象的类型 {}".format(type(img)))
print(img.shape)
print("图像宽度: {} pixels".format(img.shape[1]))
print("图像高度: {} pixels".format(img.shape[0]))
# GRAYScale 没有第三个维度哦, 所以这样会报错
# print("通道: {}".format(img.shape[2]))
print("图像分辨率: {}".format(img.size))
print("数据类型: {}".format(img.dtype))
if __name__ == '__main__':
# 导入一张图像 模式为彩色图片
img_path = sys.argv[1]
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
# 将色彩空间转变为灰度图并展示
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 打印图片信息
# print_img_info(gray)
# 打印图片的局部
# print("打印图片局部")
# print(gray[100:105, 100:105])
# plt.imshow(gray)
# 需要添加colormap 颜色映射函数为gray
plt.imshow(gray, cmap="gray")
# 隐藏坐标系
plt.axis('off')
# 展示图片
# plt.show()
# 你也可以保存图片, 填入图片路径就好
new_name = '%s-gray.%s' % (img_path.split('.')[0], img_path.split('.')[-1])
plt.savefig(new_name)
print(new_name)
plt.close()

85
scripts/ins2mastodon.py Executable file
View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# scraper instagram posts
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
import pickle
from urllib.parse import urlparse
import time
from mastodon import Mastodon
from instagram_private_api import Client
import requests
import logging
from dsite import settings
logging.basicConfig(filename='/root/develop/log/ins2mastodon.log', level=logging.INFO)
logger = logging.getLogger('/root/develop/log/ins2mastodon.log')
mastodon_cli = Mastodon(access_token=settings.MASTODON_NOFAN_ACCESS_TOKEN, api_base_url='https://nofan.xyz')
def send_image_to_mastodon(image_url, text):
resp = requests.get(image_url)
mime_type = 'image/jpeg'
url = urlparse(image_url)
ext = url.path.split('.')[-1]
if ext == 'gif':
mime_type = 'image/gif'
toot_resp = mastodon_cli.media_post(resp.content, mime_type)
if toot_resp.get('id'):
media_ids = [toot_resp['id']]
mastodon_cli.status_post(text, media_ids=media_ids)
logger.info('send %s', text)
image_name = url.path.split('/')[-1]
with open(settings.MASTODON_SYNCED_IMAGES_LOG, 'a') as f:
f.write(image_name + '\n')
# write binary file with api.settings
def writeSettings(user, pwd, settings_file):
api = Client(user, pwd)
with open(settings_file, "wb") as FileObj:
pickle.dump(api.settings, FileObj)
# read binary file to api.settings
def readSettings(settings_file):
cache = None
with open(settings_file, "rb") as FileObj:
cache = pickle.load(FileObj)
return cache
if __name__ == '__main__':
if not os.path.exists(settings.IG_PRIVATE_API_SETTINGS):
writeSettings(settings.IG_LOGIN_USERNAME, settings.IG_LOGIN_PASSWORD, settings.IG_PRIVATE_API_SETTINGS)
cache_settings = readSettings(settings.IG_PRIVATE_API_SETTINGS)
api = Client(settings.IG_LOGIN_USERNAME, settings.IG_LOGIN_PASSWORD, settings=cache_settings)
while True:
results = api.self_feed()
logger.info('getting %s posts', len(results['items']))
for item in results['items']:
text = item['caption']['text']
image_url = item['image_versions2']['candidates'][0]['url']
try:
with open(settings.MASTODON_SYNCED_IMAGES_LOG, 'r') as f:
send_images = f.readlines()
send_images = [x.strip() for x in send_images]
image_name = urlparse(image_url).path.split('/')[-1]
if image_name not in send_images:
send_image_to_mastodon(image_url, text)
except Exception as e:
logger.error(e)
time.sleep(60)

42
scripts/ins_scraper.py Executable file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# scraper instagram posts
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
import instagram_scraper
import time
from dsite import settings
if __name__ == '__main__':
args = {
'login_user': settings.IG_LOGIN_USERNAME,
'login_pass': settings.IG_LOGIN_PASSWORD,
'cookiejar': settings.IG_SCRAPER_COOKIE_FILE,
'latest_stamps': settings.IG_SCRAPER_LAST_STAMP_FILE,
'usernames': settings.IG_SCRAPER_USERNAMES,
'destination': settings.IG_SCRAPER_DESTINATION,
}
scraper = instagram_scraper.InstagramScraper(**args)
scraper.authenticate_with_login()
scraper.save_cookies()
while True:
time.sleep(10)
args_ = {
'cookiejar': settings.IG_SCRAPER_COOKIE_FILE,
'latest_stamps': settings.IG_SCRAPER_LAST_STAMP_FILE,
'usernames': settings.IG_SCRAPER_USERNAMES,
'destination': settings.IG_SCRAPER_DESTINATION,
'template': '{username}-{datetime}-{shortcode}',
}
scraper = instagram_scraper.InstagramScraper(**args_)
scraper.scrape()

5
scripts/restart_dodo.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
supervisorctl restart dodo && \
curl -X POST -H "Content-Type: application/json" \
-d '{"msg_type":"text","content":{"text":"🎉 dodo 部署成功 🎉"}}' \
https://open.feishu.cn/open-apis/bot/v2/hook/57cfa603-6154-4055-a739-210028171d10

View File

@ -12,7 +12,6 @@ class OfficeHoursForm(forms.ModelForm):
self.user = kwargs.pop('user')
return super().__init__(*args, **kwargs)
def full_clean(self):
if not self.user.is_authenticated:
raise forms.ValidationError('Invalid User.')
@ -22,9 +21,6 @@ class OfficeHoursForm(forms.ModelForm):
begins_at = timer.models.OfficeHours.parse_time_str(self.data.get('begins_at'))
user = self.user
obj = timer.models.OfficeHours.objects.create(
user=user,
begins_at=begins_at,
ends_at=timer.models.OfficeHours.get_ends_at(begins_at))
user=user, begins_at=begins_at, ends_at=timer.models.OfficeHours.get_ends_at(begins_at)
)
return obj

View File

@ -9,9 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
@ -22,5 +20,5 @@ class Migration(migrations.Migration):
('ends_at', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
)
]

View File

@ -1,4 +1,5 @@
from django.conf.urls import include, url
# from django.core.urlresolvers import reverse
from django.urls import path
from rest_framework import routers

View File

@ -19,9 +19,11 @@ from django.urls import reverse
class OfficeHoursAPI(CreateAPIView):
authentication_classes = (authentication.TokenAuthentication,
authentication_classes = (
authentication.TokenAuthentication,
authentication.SessionAuthentication,
authentication.BasicAuthentication)
authentication.BasicAuthentication,
)
permission_classes = (permissions.IsAuthenticated,)
queryset = timer.models.OfficeHours.objects.all()
@ -34,16 +36,12 @@ class OfficeHoursAPI(CreateAPIView):
begins_at = timer.models.OfficeHours.parse_time_str(begins_at)
ends_at = timer.models.OfficeHours.get_ends_at(begins_at)
oh, __ = timer.models.OfficeHours.objects.get_or_create(
begins_at=begins_at,
ends_at=ends_at,
user=request.user)
begins_at=begins_at, ends_at=ends_at, user=request.user
)
except ValueError:
raise
oh.refresh_from_db()
resp_data = {
'begins_at': oh.get_begins_at_str,
'ends_at': oh.get_ends_at_str,
}
resp_data = {'begins_at': oh.get_begins_at_str, 'ends_at': oh.get_ends_at_str}
return Response(resp_data, status=status.HTTP_201_CREATED)

View File

@ -59,8 +59,7 @@ def day_start(date):
Returns:
返回 date 这天的开始时间0 `Datetime` 对象
"""
return timezone.localtime(date).replace(hour=0, minute=0, second=0,
microsecond=0)
return timezone.localtime(date).replace(hour=0, minute=0, second=0, microsecond=0)
def week_start(date):
@ -73,8 +72,8 @@ def week_start(date):
返回 date 这天所在周开始时间周一`Datetime` 对象
"""
return timezone.localtime(date) + dateutil.relativedelta.relativedelta(
weekday=dateutil.relativedelta.MO(-1), hour=0, minute=0, second=0,
microsecond=0)
weekday=dateutil.relativedelta.MO(-1), hour=0, minute=0, second=0, microsecond=0
)
def month_start(d):
@ -87,7 +86,8 @@ def month_start(d):
返回 date 这天所在月的开始时间1`Datetime` 对象
"""
return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
day=1, hour=0, minute=0, second=0, microsecond=0)
day=1, hour=0, minute=0, second=0, microsecond=0
)
def month_end(d):
@ -114,5 +114,5 @@ def year_start(d):
返回 date 这天所在年的开始时间1 1`Datetime` 对象
"""
return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)

View File

@ -11,14 +11,14 @@ RECIPE_TYPE_MEAT = 'meat'
RECIPE_TYPE_VEGETABLE = 'vegetable'
RECIPE_TYPE_SOUP = 'soup'
RECIPE_TYPE_CHOICE = [
RECIPE_TYPE_MEAT,
RECIPE_TYPE_VEGETABLE,
RECIPE_TYPE_SOUP,
]
RECIPE_STATUS_ACTIVE = 'active'
RECIPE_STATUS_DELETED = 'deleted'
RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP]
FILTER_EXACT = ['exact']
FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt']
LARK_WEBHOOK_MSG_TYPE_TEXT = 'text'
LARK_WEBHOOK_MSG_TYPE_INTERACTIVE = 'interactive'

View File

@ -2,6 +2,7 @@
import sys
import os
from imp import reload
sys.path.insert(0, os.path.abspath('..'))
sys.path.append('/Users/ching/develop/dsite')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "../dsite.settings")
@ -16,10 +17,5 @@ if __name__ == '__main__':
repo = git.Repo(search_parent_directories=True)
commit = repo.head.commit
rev, branch = commit.name_rev.split(' ')
msg = 'rev: %s\n\nauther: %s\n\nbranch: %s\n\nmessage: %s' % (
rev,
commit.author.name,
branch,
commit.summary
)
msg = 'rev: %s\n\nauther: %s\n\nbranch: %s\n\nmessage: %s' % (rev, commit.author.name, branch, commit.summary)
print(utils.lark.request({'text': msg}))

View File

@ -22,15 +22,16 @@ def gen_sign(timestamp, secret):
return sign
def request(content, msg_type=const.LARK_WEBHOOK_MSG_TYPE_TEXT):
""" content: {'text': 'xxxxx}
"""
"""content: {'text': 'xxxxx}"""
timestamp = utils.timestamp_of(now())
data = {
"timestamp": timestamp,
"sign": gen_sign(timestamp, settings.LARK_WEBHOOK_SECRET),
"msg_type": msg_type,
"content": content}
"content": content,
}
resp = requests.post(settings.LARK_WEBHOOK_URL, data=json.dumps(data))
if resp.status_code == 200 and resp.json().get('StatusCode') == 0:
return True