diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b8fa15..a07a049 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,7 @@ "editor.formatOnSave": true, "editor.rulers": [ 120 - ] + ], + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": "active" } diff --git a/develop_requirements.txt b/develop_requirements.txt index 89452a9..c71d62c 100644 --- a/develop_requirements.txt +++ b/develop_requirements.txt @@ -31,3 +31,4 @@ user-agents==2.2.0 wcwidth==0.2.5 zipp==3.5.0 redis==4.1.0 +black diff --git a/frontend/src/components/input_recipe.vue b/frontend/src/components/input_recipe.vue index a8535d4..b052536 100644 --- a/frontend/src/components/input_recipe.vue +++ b/frontend/src/components/input_recipe.vue @@ -1,5 +1,5 @@ @@ -130,4 +192,7 @@ export default { .recipe-create { margin: 20px 16px; } +.submit-button { + width: 100%; +} diff --git a/recipe/migrations/0003_recipe_status.py b/recipe/migrations/0003_recipe_status.py new file mode 100644 index 0000000..8ecba17 --- /dev/null +++ b/recipe/migrations/0003_recipe_status.py @@ -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), + ), + ] diff --git a/recipe/models.py b/recipe/models.py index 2cc9982..52b566f 100644 --- a/recipe/models.py +++ b/recipe/models.py @@ -9,6 +9,7 @@ import utils class Recipe(models.Model): name = models.CharField(max_length=128) 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) @@ -54,7 +55,9 @@ class Recipe(models.Model): difficulty = 0 else: difficulty = 0 - recipe = cls.objects.create(name=name, recipe_type=recipe_type, rate=rate, difficulty=difficulty) + recipe = cls.objects.create( + name=name, recipe_type=recipe_type, rate=rate, difficulty=difficulty, status=const.RECIPE_STATUS_ACTIVE + ) return recipe @property @@ -125,7 +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 @@ -138,7 +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 @@ -150,7 +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) diff --git a/recipe/views.py b/recipe/views.py index 175c057..436402b 100644 --- a/recipe/views.py +++ b/recipe/views.py @@ -12,7 +12,7 @@ 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, @@ -21,6 +21,10 @@ 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): @@ -28,7 +32,7 @@ class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics # 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, diff --git a/scripts/bitwarden_backup.py b/scripts/bitwarden_backup.py new file mode 100644 index 0000000..31f501a --- /dev/null +++ b/scripts/bitwarden_backup.py @@ -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) diff --git a/scripts/dodo.py b/scripts/dodo.py index b3628a5..c0783be 100644 --- a/scripts/dodo.py +++ b/scripts/dodo.py @@ -224,6 +224,11 @@ 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('/菜谱 '): diff --git a/scripts/dsite_prepare.sh b/scripts/dsite_prepare.sh new file mode 100644 index 0000000..f3a14aa --- /dev/null +++ b/scripts/dsite_prepare.sh @@ -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 diff --git a/scripts/restart_dodo.sh b/scripts/restart_dodo.sh new file mode 100644 index 0000000..9b484cb --- /dev/null +++ b/scripts/restart_dodo.sh @@ -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 diff --git a/utils/const.py b/utils/const.py index bdfc767..ff00587 100644 --- a/utils/const.py +++ b/utils/const.py @@ -11,6 +11,9 @@ RECIPE_TYPE_MEAT = 'meat' RECIPE_TYPE_VEGETABLE = 'vegetable' RECIPE_TYPE_SOUP = 'soup' +RECIPE_STATUS_ACTIVE = 'active' +RECIPE_STATUS_DELETED = 'deleted' + RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP] FILTER_EXACT = ['exact']