248 lines
8.8 KiB
Python
248 lines
8.8 KiB
Python
import random
|
||
from django.db import models
|
||
from django.utils.timezone import now, localtime
|
||
import datetime
|
||
from utils import const
|
||
import utils
|
||
|
||
|
||
class Ingredient(models.Model):
|
||
name = models.CharField(max_length=128)
|
||
unit = models.CharField(max_length=32)
|
||
status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE)
|
||
|
||
|
||
class RecipeIngredient(models.Model):
|
||
recipe = models.ForeignKey('Recipe', on_delete=models.CASCADE, related_name='recipe_ingredients')
|
||
ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE, related_name='recipe_ingredients')
|
||
status = models.CharField(max_length=32, default=const.INGREDIENT_STATUS_ACTIVE)
|
||
quantity = models.FloatField()
|
||
|
||
def serialize(self):
|
||
return {
|
||
'id': self.id,
|
||
'recipe': {'id': self.recipe.id, 'name': self.recipe.name},
|
||
'ingredient': {'id': self.ingredient.id, 'name': self.ingredient.name, 'unit': self.ingredient.unit},
|
||
'quantity': self.quantity,
|
||
}
|
||
|
||
|
||
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)
|
||
|
||
def serialize(self, verbose=False):
|
||
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,
|
||
'recipe_ingredients': self.get_ingredients(),
|
||
}
|
||
)
|
||
|
||
return data
|
||
|
||
def get_ingredients(self):
|
||
return [i.serialize() for i in self.recipeingredient_set.order_by('id')]
|
||
|
||
@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
|
||
|
||
@classmethod
|
||
def sum_recipe_ingredients(cls, recipe_ids):
|
||
data = {}
|
||
for recipe_id in recipe_ids:
|
||
recipe = cls.objects.get(id=recipe_id)
|
||
recipe_ingredients = RecipeIngredient.objects.filter(recipe=recipe)
|
||
for ingredient in recipe_ingredients:
|
||
if not ingredient.ingredient.id in data:
|
||
data[ingredient.ingredient.id] = {
|
||
'name': ingredient.ingredient.name,
|
||
'quantity': 0,
|
||
'unit': ingredient.ingredient.unit,
|
||
}
|
||
data[ingredient.ingredient.id]['quantity'] += ingredient.quantity
|
||
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)
|
||
|
||
def generate_recipe(self, prev_recipes=None, ignore_prev=False):
|
||
if not prev_recipes:
|
||
prev_recipes = []
|
||
if ignore_prev:
|
||
prev_recipes = []
|
||
|
||
recipes = []
|
||
retry = 5
|
||
|
||
# meat
|
||
for x in range(0, 2):
|
||
while True:
|
||
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
|
||
else:
|
||
retry -= 1
|
||
if retry <= 0:
|
||
retry = 5
|
||
break
|
||
|
||
# vegetable
|
||
for x in range(0, 1):
|
||
while True:
|
||
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
|
||
else:
|
||
retry -= 1
|
||
if retry <= 0:
|
||
retry = 5
|
||
break
|
||
|
||
# soup
|
||
if random.randint(0, 2):
|
||
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)
|
||
|
||
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: []}
|
||
for key_, value_ in data.items():
|
||
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)
|
||
data['date'] = utils.timestamp_of(utils.day_start(date))
|
||
data['id'] = self.id
|
||
return data
|
||
|
||
@classmethod
|
||
def get_week_recipe_data(cls):
|
||
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')
|
||
data = [{}] * (7 - len(daily_recipes))
|
||
|
||
for daily_recipe in daily_recipes:
|
||
data.append(daily_recipe.serialize())
|
||
return data
|