Compare commits
23 Commits
fe77a53322
...
e2ed14784a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2ed14784a | ||
|
|
2280fd25c1 | ||
|
|
b017b691ee | ||
|
|
3e206522e8 | ||
|
|
4a940491fe | ||
|
|
13942425aa | ||
|
|
3f7e61d7dc | ||
|
|
e82ac3ae3d | ||
|
|
70cccac644 | ||
|
|
95f70c1478 | ||
|
|
23f813f592 | ||
|
|
047bd00545 | ||
|
|
78ef05b68e | ||
|
|
a8fff9c533 | ||
|
|
4a6d2f82e0 | ||
|
|
d63f494594 | ||
|
|
973849540a | ||
|
|
0228221b91 | ||
|
|
446b1dc2e5 | ||
|
|
e95ca6089d | ||
|
|
50fb4bada4 | ||
|
|
018ff7c3a0 | ||
|
|
c89b052578 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,6 +6,9 @@ __pycache__/
|
|||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
# settings
|
||||||
|
dsite/settings.py
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
|
|||||||
17
Jenkinsfile
vendored
Normal file
17
Jenkinsfile
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
pipeline {
|
||||||
|
agent {
|
||||||
|
docker {
|
||||||
|
image 'node:6-alpine'
|
||||||
|
args '-p 4000:4000'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
stages {
|
||||||
|
stage('Build') {
|
||||||
|
steps {
|
||||||
|
sh 'npm install'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,8 +7,8 @@ django-cors-headers==3.8.0
|
|||||||
django-filter==2.4.0
|
django-filter==2.4.0
|
||||||
djangorestframework==3.12.4
|
djangorestframework==3.12.4
|
||||||
importlib-metadata==4.6.4
|
importlib-metadata==4.6.4
|
||||||
ipdb==0.13.9
|
ipdb
|
||||||
ipython==7.16.1
|
ipython
|
||||||
ipython-genutils==0.2.0
|
ipython-genutils==0.2.0
|
||||||
jedi==0.17.2
|
jedi==0.17.2
|
||||||
Markdown==3.3.4
|
Markdown==3.3.4
|
||||||
@ -30,3 +30,4 @@ ua-parser==0.10.0
|
|||||||
user-agents==2.2.0
|
user-agents==2.2.0
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
zipp==3.5.0
|
zipp==3.5.0
|
||||||
|
redis==4.1.0
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
from .settings_default import *
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
|
||||||
|
|
||||||
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
|
|
||||||
# CORS_ALLOW_ALL_ORIGINS = True # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
|
|
||||||
CORS_ALLOWED_ORIGINS = [
|
|
||||||
'http://localhost:8080',
|
|
||||||
'http://127.0.0.1:8080',
|
|
||||||
'http://192.168.1.101:8080'
|
|
||||||
]
|
|
||||||
|
|
||||||
CORS_ALLOW_METHODS = [
|
|
||||||
"DELETE",
|
|
||||||
"GET",
|
|
||||||
"OPTIONS",
|
|
||||||
"PATCH",
|
|
||||||
"POST",
|
|
||||||
"PUT",
|
|
||||||
]
|
|
||||||
|
|
||||||
CORS_ALLOW_HEADERS = [
|
|
||||||
"accept",
|
|
||||||
"accept-encoding",
|
|
||||||
"authorization",
|
|
||||||
"content-type",
|
|
||||||
"dnt",
|
|
||||||
"origin",
|
|
||||||
"user-agent",
|
|
||||||
"x-csrftoken",
|
|
||||||
"x-requested-with",
|
|
||||||
]
|
|
||||||
|
|
||||||
CORS_ALLOW_HEADERS = ('*')
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
|
||||||
os.path.join(BASE_DIR, "frontend/dist/"),
|
|
||||||
]
|
|
||||||
|
|
||||||
MEDIA_URL = '/media/'
|
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
|
||||||
@ -155,3 +155,7 @@ REST_FRAMEWORK = {
|
|||||||
CORS_ALLOWED_ORIGINS = []
|
CORS_ALLOWED_ORIGINS = []
|
||||||
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
|
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
|
||||||
# CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
|
# CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
|
||||||
|
|
||||||
|
|
||||||
|
LARK_WEBHOOK_URL = ''
|
||||||
|
LARK_WEBHOOK_SECRET = ''
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
.summit-recipe[data-v-16b31338]{width:100%}.el-pagination{margin:10px 0 0 0}.el-row{margin-bottom:20px;&:last-child{margin-bottom:0}}.el-col,.grid-content{border-radius:4px}.grid-content{min-height:36px}.row-bg{padding:10px 0;background-color:#f9fafc}.content{padding:20px 10px}.re-generate{margin:20px 0;width:100%}.el-tag#meal a:link,.el-tag#meal a:visited{text-decoration:none}.el-tag#meal a:active,.el-tag#meal a:hover{text-decoration:underline}.el-tag{margin:0 5px 0 0}.el-tag+.el-tag{margin:5px 0 0 0}.recipe-type-tag{margin:0 10px 0 0;padding-bottom:0}.recipe-list{height:50%}body{background-color:#f7f8fa}.recipe-create{margin:20px 16px}.recipe_type,.recipe_type .van-grid-item__content{background-color:#f2f3f5}.daily_recipes{margin-bottom:10px}.daily_recipes:last-child{margin-bottom:40px}.action-button{height:100%;width:50%}.daily_recipe{margin-top:20px}.delete-button{width:100%;height:100%}.delete-icon{font-size:16px;line-height:inherit;padding:0 5px}
|
.summit-recipe[data-v-16b31338]{width:100%}.el-pagination{margin:10px 0 0 0}.el-row{margin-bottom:20px;&:last-child{margin-bottom:0}}.el-col,.grid-content{border-radius:4px}.grid-content{min-height:36px}.row-bg{padding:10px 0;background-color:#f9fafc}.content{padding:20px 10px}.re-generate{margin:20px 0;width:100%}.el-tag#meal a:link,.el-tag#meal a:visited{text-decoration:none}.el-tag#meal a:active,.el-tag#meal a:hover{text-decoration:underline}.el-tag{margin:0 5px 0 0}.el-tag+.el-tag{margin:5px 0 0 0}.recipe-type-tag{margin:0 10px 0 0;padding-bottom:0}.recipe-list{height:50%}body{background-color:#f7f8fa}.recipe-create{margin:20px 16px}.recipe_type,.recipe_type .van-grid-item__content{background-color:#f2f3f5}.daily_recipes{margin-bottom:10px}.daily_recipes:last-child{margin-bottom:40px}.action-button{height:100%;width:50%}.today-tag{margin-left:10px}.refresh-button{float:right;margin-right:20px}.daily_recipe{margin-top:20px}.delete-button{width:100%;height:100%}.delete-icon{font-size:16px;line-height:inherit;padding:0 5px}
|
||||||
File diff suppressed because one or more lines are too long
30
frontend/dist/index.html
vendored
30
frontend/dist/index.html
vendored
@ -1,29 +1 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>frontend</title><link href="/css/app.54db996e.css" rel="preload" as="style"><link href="/css/chunk-vendors.920d8cf8.css" rel="preload" as="style"><link href="/js/app.30780f00.js" rel="preload" as="script"><link href="/js/chunk-vendors.6cd9f85d.js" rel="preload" as="script"><link href="/css/chunk-vendors.920d8cf8.css" rel="stylesheet"><link href="/css/app.54db996e.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.6cd9f85d.js"></script><script src="/js/app.30780f00.js"></script></body></html>
|
||||||
<html lang="">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=0"
|
|
||||||
/>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<title>frontend</title>
|
|
||||||
<link href="/css/app.12f77eb5.css" rel="preload" as="style" />
|
|
||||||
<link href="/css/chunk-vendors.b9153273.css" rel="preload" as="style" />
|
|
||||||
<link href="/js/app.4a8d2f87.js" rel="preload" as="script" />
|
|
||||||
<link href="/js/chunk-vendors.4d54ba89.js" rel="preload" as="script" />
|
|
||||||
<link href="/css/chunk-vendors.b9153273.css" rel="stylesheet" />
|
|
||||||
<link href="/css/app.12f77eb5.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript
|
|
||||||
><strong
|
|
||||||
>We're sorry but frontend doesn't work properly without JavaScript
|
|
||||||
enabled. Please enable it to continue.</strong
|
|
||||||
></noscript
|
|
||||||
>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="/js/chunk-vendors.4d54ba89.js"></script>
|
|
||||||
<script src="/js/app.4a8d2f87.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
2
frontend/dist/js/app.30780f00.js
vendored
Normal file
2
frontend/dist/js/app.30780f00.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/js/app.30780f00.js.map
vendored
Normal file
1
frontend/dist/js/app.30780f00.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
frontend/dist/js/app.4a8d2f87.js
vendored
2
frontend/dist/js/app.4a8d2f87.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/js/app.4a8d2f87.js.map
vendored
1
frontend/dist/js/app.4a8d2f87.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
frontend/dist/js/chunk-vendors.6cd9f85d.js.map
vendored
Normal file
1
frontend/dist/js/chunk-vendors.6cd9f85d.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16387
frontend/package-lock.json
generated
16387
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,9 +14,9 @@
|
|||||||
"axios": "^0.21.2",
|
"axios": "^0.21.2",
|
||||||
"bootstrap": "^5.1.1",
|
"bootstrap": "^5.1.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"element-plus": "^1.0.2-beta.28",
|
"element-plus": "^1.3.0-beta.5",
|
||||||
"vant": "^3.2.4",
|
"vant": "^3.2.4",
|
||||||
"vue": "^3.2.2",
|
"vue": "^3.1",
|
||||||
"vue-router": "^4.0.11"
|
"vue-router": "^4.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<van-pull-refresh
|
<!-- <van-pull-refresh
|
||||||
v-model="loading"
|
v-model="loading"
|
||||||
@refresh="onRefresh"
|
@refresh="onRefresh"
|
||||||
pulling-text="下拉重新生成每周菜谱"
|
pulling-text="下拉重新生成每周菜谱"
|
||||||
loosing-text="可以松手了..."
|
loosing-text="可以松手了..."
|
||||||
>
|
pull-distance="100"
|
||||||
|
:head-height="100"
|
||||||
|
> -->
|
||||||
<div v-for:="(daily_recipe, recipe_index) in daily_recipes">
|
<div v-for:="(daily_recipe, recipe_index) in daily_recipes">
|
||||||
<van-cell-group
|
<van-cell-group inset class="daily_recipes">
|
||||||
inset
|
<template #title>
|
||||||
:title="formatDate(daily_recipe.date)"
|
<span> {{ formatDate(daily_recipe.date) }}</span>
|
||||||
class="daily_recipes"
|
<van-tag
|
||||||
|
type="danger"
|
||||||
|
class="today-tag"
|
||||||
|
v-if="isToday(daily_recipe.date)"
|
||||||
|
>今天</van-tag
|
||||||
>
|
>
|
||||||
|
</template>
|
||||||
<van-swipe-cell>
|
<van-swipe-cell>
|
||||||
<van-row>
|
<van-row v-if="daily_recipe.meat">
|
||||||
<van-col span="3" class="recipe_type">
|
<van-col span="3" class="recipe_type">
|
||||||
<van-grid :column-num="1">
|
<van-grid :column-num="1">
|
||||||
<van-grid-item text="肉" class="recipe_type"></van-grid-item>
|
<van-grid-item text="肉" class="recipe_type"></van-grid-item>
|
||||||
@ -35,7 +42,7 @@
|
|||||||
</van-grid>
|
</van-grid>
|
||||||
</van-col>
|
</van-col>
|
||||||
</van-row>
|
</van-row>
|
||||||
<van-row>
|
<van-row v-if="daily_recipe.vegetable">
|
||||||
<van-col span="3" class="recipe_type">
|
<van-col span="3" class="recipe_type">
|
||||||
<van-grid :column-num="1">
|
<van-grid :column-num="1">
|
||||||
<van-grid-item text="菜" class="recipe_type"></van-grid-item>
|
<van-grid-item text="菜" class="recipe_type"></van-grid-item>
|
||||||
@ -58,7 +65,9 @@
|
|||||||
</van-grid>
|
</van-grid>
|
||||||
</van-col>
|
</van-col>
|
||||||
</van-row>
|
</van-row>
|
||||||
<van-row v-if="daily_recipe.soup.length > 0">
|
<van-row
|
||||||
|
v-if="daily_recipe.soup !== undefined && daily_recipe.soup.length > 0"
|
||||||
|
>
|
||||||
<van-col span="3" class="recipe_type">
|
<van-col span="3" class="recipe_type">
|
||||||
<van-grid :column-num="1">
|
<van-grid :column-num="1">
|
||||||
<van-grid-item text="汤" class="recipe_type"></van-grid-item>
|
<van-grid-item text="汤" class="recipe_type"></van-grid-item>
|
||||||
@ -99,7 +108,17 @@
|
|||||||
</van-swipe-cell>
|
</van-swipe-cell>
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</div>
|
</div>
|
||||||
</van-pull-refresh>
|
<!-- </van-pull-refresh> -->
|
||||||
|
<van-sticky :offset-bottom="60" position="bottom" class="refresh-button">
|
||||||
|
<van-button
|
||||||
|
type="primary"
|
||||||
|
color="linear-gradient(to right, #ff6034, #ee0a24)"
|
||||||
|
hairline
|
||||||
|
round
|
||||||
|
@click="onRefresh"
|
||||||
|
>刷新</van-button
|
||||||
|
>
|
||||||
|
</van-sticky>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -113,6 +132,8 @@ import {
|
|||||||
PullRefresh,
|
PullRefresh,
|
||||||
Row,
|
Row,
|
||||||
SwipeCell,
|
SwipeCell,
|
||||||
|
Tag,
|
||||||
|
Sticky,
|
||||||
} from 'vant';
|
} from 'vant';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import config from '@/config/index';
|
import config from '@/config/index';
|
||||||
@ -120,6 +141,7 @@ import config from '@/config/index';
|
|||||||
export default {
|
export default {
|
||||||
name: 'RecipeMobileWeekRecipe',
|
name: 'RecipeMobileWeekRecipe',
|
||||||
components: {
|
components: {
|
||||||
|
[Tag.name]: Tag,
|
||||||
[Button.name]: Button,
|
[Button.name]: Button,
|
||||||
[Cell.name]: Cell,
|
[Cell.name]: Cell,
|
||||||
[CellGroup.name]: CellGroup,
|
[CellGroup.name]: CellGroup,
|
||||||
@ -129,6 +151,7 @@ export default {
|
|||||||
[PullRefresh.name]: PullRefresh,
|
[PullRefresh.name]: PullRefresh,
|
||||||
[Row.name]: Row,
|
[Row.name]: Row,
|
||||||
[SwipeCell.name]: SwipeCell,
|
[SwipeCell.name]: SwipeCell,
|
||||||
|
[Sticky.name]: Sticky,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -140,13 +163,29 @@ export default {
|
|||||||
this.getWeekRecipe(false);
|
this.getWeekRecipe(false);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isToday(date) {
|
||||||
|
const today = new Date();
|
||||||
|
var date_ = new Date(date * 1000);
|
||||||
|
if (
|
||||||
|
date_.getDate() == today.getDate() &&
|
||||||
|
date_.getMonth() == today.getMonth() &&
|
||||||
|
date_.getFullYear() == today.getFullYear()
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
if (date === undefined) {
|
if (date === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var date_ = new Date(date * 1000);
|
var date_ = new Date(date * 1000);
|
||||||
var days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
var days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||||
return days[date_.getDay()];
|
var date_str = days[date_.getDay()];
|
||||||
|
if (this.isToday(date_)) {
|
||||||
|
date_str += ' 🌟';
|
||||||
|
}
|
||||||
|
return date_str;
|
||||||
},
|
},
|
||||||
onRefresh() {
|
onRefresh() {
|
||||||
this.getWeekRecipe(true);
|
this.getWeekRecipe(true);
|
||||||
@ -197,4 +236,11 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
.today-tag {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
float: right;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,12 +2,19 @@
|
|||||||
<el-row justify="center">
|
<el-row justify="center">
|
||||||
<el-col>
|
<el-col>
|
||||||
<el-table border stripe :data="daily_recipes" max-height="500">
|
<el-table border stripe :data="daily_recipes" max-height="500">
|
||||||
<el-table-column
|
<el-table-column label="日期">
|
||||||
prop="date"
|
<template #default="scope">
|
||||||
label="日期"
|
{{ formatDate(scope.row, scope.column, scope.row.date) }}
|
||||||
:formatter="formatDate"
|
<el-tag
|
||||||
width="50px"
|
v-if:="isToday(scope.row.date)"
|
||||||
></el-table-column>
|
:key="scope"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
effect="dark"
|
||||||
|
>今天</el-tag
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="meat" label="肉">
|
<el-table-column prop="meat" label="肉">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag
|
<el-tag
|
||||||
@ -87,11 +94,25 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
console.log(config.publicPath);
|
||||||
|
console.log(process.env);
|
||||||
axios
|
axios
|
||||||
.get(config.publicPath + '/recipe/week-recipe/')
|
.get(config.publicPath + '/recipe/week-recipe/')
|
||||||
.then((response) => (this.daily_recipes = response.data));
|
.then((response) => (this.daily_recipes = response.data));
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isToday(date) {
|
||||||
|
const today = new Date();
|
||||||
|
var date_ = new Date(date * 1000);
|
||||||
|
if (
|
||||||
|
date_.getDate() == today.getDate() &&
|
||||||
|
date_.getMonth() == today.getMonth() &&
|
||||||
|
date_.getFullYear() == today.getFullYear()
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
reGenerateRecipe() {
|
reGenerateRecipe() {
|
||||||
axios
|
axios
|
||||||
.post(config.publicPath + '/recipe/week-recipe/')
|
.post(config.publicPath + '/recipe/week-recipe/')
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/lib/theme-chalk/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import locale from 'element-plus/lib/locale/lang/zh-cn'
|
import locale from 'element-plus/lib/locale/lang/zh-cn'
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
|
|||||||
4204
frontend/yarn.lock
4204
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -51,8 +51,8 @@ class WeekRecipeListAPI(rest_framework.generics.ListAPIView,
|
|||||||
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(
|
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(
|
||||||
date=today + datetime.timedelta(days=x)
|
date=today + datetime.timedelta(days=x)
|
||||||
)
|
)
|
||||||
daily_recipe.generate_recipe()
|
daily_recipe.generate_recipe(recipes)
|
||||||
recipes.append(daily_recipe.recipes.values_list('id', flat=True))
|
recipes.extend(daily_recipe.recipes.values_list('id', flat=True))
|
||||||
return Response(recipe.models.DailyRecipe.get_week_recipe_data(),
|
return Response(recipe.models.DailyRecipe.get_week_recipe_data(),
|
||||||
status=status.HTTP_201_CREATED, headers={})
|
status=status.HTTP_201_CREATED, headers={})
|
||||||
|
|
||||||
|
|||||||
285
scripts/dodo.py
Normal file
285
scripts/dodo.py
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# --coding:utf-8--
|
||||||
|
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
import json
|
||||||
|
from urllib import request
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
from mastodon import Mastodon
|
||||||
|
import logging
|
||||||
|
import redis
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import subprocess
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3'
|
||||||
|
APP_ID = 'cli_a115fe8b83f9100c'
|
||||||
|
APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u'
|
||||||
|
ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE'
|
||||||
|
EVENT_TYPE = ['im.message.receive_v1']
|
||||||
|
ADD_GROUP_NAME = True
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
data = bytes(json.dumps(req_body), encoding='utf8')
|
||||||
|
req = request.Request(url=url, data=data, headers=headers, method='POST')
|
||||||
|
try:
|
||||||
|
response = request.urlopen(req)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('get tenant token error: %s', e.read().decode())
|
||||||
|
return ""
|
||||||
|
|
||||||
|
rsp_body = response.read().decode('utf-8')
|
||||||
|
rsp_dict = json.loads(rsp_body)
|
||||||
|
code = rsp_dict.get("code", -1)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
except:
|
||||||
|
# todo: log
|
||||||
|
return
|
||||||
|
return group_name
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_POST(self):
|
||||||
|
# 解析请求 body
|
||||||
|
req_body = self.rfile.read(int(self.headers['content-length']))
|
||||||
|
obj = json.loads(req_body.decode("utf-8"))
|
||||||
|
cipher = AESCipher(ENCRYPT_KEY)
|
||||||
|
obj = json.loads(cipher.decrypt_string(obj['encrypt']))
|
||||||
|
logger.info('lark request body: %s', obj)
|
||||||
|
|
||||||
|
# 校验 verification token 是否匹配,token 不匹配说明该回调并非来自开发平台
|
||||||
|
token = obj.get("token", "")
|
||||||
|
if not token:
|
||||||
|
token = obj.get('header', {}).get('token', '')
|
||||||
|
if token != APP_VERIFICATION_TOKEN:
|
||||||
|
logger.error("verification token not match, token =%s", token)
|
||||||
|
self.response("")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 根据 type 处理不同类型事件
|
||||||
|
type = obj.get("type", "")
|
||||||
|
if not type:
|
||||||
|
type = obj.get('header', {}).get('event_type')
|
||||||
|
if "url_verification" == type: # 验证请求 URL 是否有效
|
||||||
|
self.handle_request_url_verify(obj)
|
||||||
|
elif type in EVENT_TYPE: # 事件回调
|
||||||
|
# 获取事件内容和类型,并进行相应处理,此处只关注给机器人推送的消息事件
|
||||||
|
event_id = obj.get('header', {}).get('event_id', '')
|
||||||
|
# 重复收到的事件不处理
|
||||||
|
if event_id and redis_cli.get(event_id):
|
||||||
|
self.response("")
|
||||||
|
return
|
||||||
|
event = obj.get("event")
|
||||||
|
if event.get("message"):
|
||||||
|
self.handle_message(event, event_id)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_request_url_verify(self, post_obj):
|
||||||
|
# 原样返回 challenge 字段内容
|
||||||
|
challenge = post_obj.get("challenge", "")
|
||||||
|
rsp = {'challenge': challenge}
|
||||||
|
self.response(json.dumps(rsp))
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_message(self, event, event_id=None):
|
||||||
|
# 此处只处理 text 类型消息,其他类型消息忽略
|
||||||
|
msg = event.get('message', {})
|
||||||
|
msg_type = msg.get("message_type", "")
|
||||||
|
if msg_type == "text":
|
||||||
|
# 调用发消息 API 之前,先要获取 API 调用凭证:tenant_access_token
|
||||||
|
access_token = get_tenant_access_token()
|
||||||
|
if access_token == "":
|
||||||
|
self.response("")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 机器人回复收到的消息
|
||||||
|
text = json.loads(msg.get('content')).get('text')
|
||||||
|
orig_text = text
|
||||||
|
if msg.get('chat_type') == 'group' and msg.get('mentions'):
|
||||||
|
open_id = {"open_chat_id": msg.get("chat_id")}
|
||||||
|
for mention in msg.get('mentions'):
|
||||||
|
text = text.replace(mention['key'], '')
|
||||||
|
text = text.lstrip()
|
||||||
|
orig_text = text
|
||||||
|
if ADD_GROUP_NAME:
|
||||||
|
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')}
|
||||||
|
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 '):
|
||||||
|
self.msg_compoment(access_token, open_id, '指令错误')
|
||||||
|
return
|
||||||
|
if orig_text == '/last':
|
||||||
|
try:
|
||||||
|
statuses = mastodon.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(''))
|
||||||
|
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'])
|
||||||
|
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
|
||||||
|
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 '):
|
||||||
|
site_ = orig_text.split('/deploy ')[1]
|
||||||
|
if site_ == 'dsite':
|
||||||
|
self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_)
|
||||||
|
subprocess.call("/root/deploy/dsite_prepare.sh")
|
||||||
|
subprocess.run(["supervisorctl", "restart", "dsite"])
|
||||||
|
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
|
||||||
|
else:
|
||||||
|
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
toot_resp = mastodon.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 ⚠️
|
||||||
|
%s
|
||||||
|
""" % json.loads(toot_resp))
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error('send toot error: %s', str(exc))
|
||||||
|
|
||||||
|
return
|
||||||
|
elif msg_type == "image":
|
||||||
|
self.response("")
|
||||||
|
return
|
||||||
|
|
||||||
|
def response(self, body):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body.encode())
|
||||||
|
|
||||||
|
def send_message(self, token, open_id, text):
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req_body = dict(req_body, **open_id) # 根据open_id判断返回域
|
||||||
|
|
||||||
|
data = bytes(json.dumps(req_body), encoding='utf8')
|
||||||
|
req = request.Request(url=url, data=data, headers=headers, method='POST')
|
||||||
|
try:
|
||||||
|
response = request.urlopen(req)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('send message error: %s', e.read().decode())
|
||||||
|
return
|
||||||
|
|
||||||
|
rsp_body = response.read().decode('utf-8')
|
||||||
|
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", ""))
|
||||||
|
|
||||||
|
def msg_compoment(self, token, open_id, text):
|
||||||
|
self.send_message(token, open_id, text)
|
||||||
|
|
||||||
|
def run():
|
||||||
|
port = 5000
|
||||||
|
server_address = ('', port)
|
||||||
|
httpd = HTTPServer(server_address, RequestHandler)
|
||||||
|
logger.info("start...")
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run()
|
||||||
0
toot/__init__.py
Normal file
0
toot/__init__.py
Normal file
3
toot/admin.py
Normal file
3
toot/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
toot/apps.py
Normal file
6
toot/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TootConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'toot'
|
||||||
0
toot/migrations/__init__.py
Normal file
0
toot/migrations/__init__.py
Normal file
1
toot/models.py
Normal file
1
toot/models.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from django.db import models
|
||||||
3
toot/tests.py
Normal file
3
toot/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
toot/views.py
Normal file
3
toot/views.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
@ -19,3 +19,6 @@ RECIPE_TYPE_CHOICE = [
|
|||||||
|
|
||||||
FILTER_EXACT = ['exact']
|
FILTER_EXACT = ['exact']
|
||||||
FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt']
|
FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt']
|
||||||
|
|
||||||
|
|
||||||
|
LARK_WEBHOOK_MSG_TYPE_TEXT = 'text'
|
||||||
|
|||||||
25
utils/depoly_notify.py
Executable file
25
utils/depoly_notify.py
Executable file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
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")
|
||||||
|
reload(sys)
|
||||||
|
|
||||||
|
import utils.lark
|
||||||
|
import git
|
||||||
|
import ipdb
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# def notify():
|
||||||
|
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
|
||||||
|
)
|
||||||
|
print(utils.lark.request({'text': msg}))
|
||||||
37
utils/lark.py
Normal file
37
utils/lark.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
|
from dsite import settings
|
||||||
|
import utils
|
||||||
|
from utils import const
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
|
||||||
|
def gen_sign(timestamp, secret):
|
||||||
|
# 拼接timestamp和secret
|
||||||
|
string_to_sign = '{}\n{}'.format(timestamp, secret)
|
||||||
|
hmac_code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()
|
||||||
|
|
||||||
|
# 对结果进行base64处理
|
||||||
|
sign = base64.b64encode(hmac_code).decode('utf-8')
|
||||||
|
|
||||||
|
return sign
|
||||||
|
|
||||||
|
def request(content, msg_type=const.LARK_WEBHOOK_MSG_TYPE_TEXT):
|
||||||
|
""" 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}
|
||||||
|
resp = requests.post(settings.LARK_WEBHOOK_URL, data=json.dumps(data))
|
||||||
|
if resp.status_code == 200 and resp.json().get('StatusCode') == 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
Loading…
x
Reference in New Issue
Block a user