class CookBookRecipe(models.Model): cookbook = CachedForeignKey('CookBook') recipe = CachedForeignKey('Recipe') note = models.CharField(_("Note"), max_length=255, blank=True) added = models.DateField(_("Added")) class Meta: unique_together = (('cookbook', 'recipe'), ) verbose_name = _("Cookbook's recipes") verbose_name = _("Cookbooks' recipes") def save(self, *args, **kwargs): if not self.pk: self.added = date.today() ret_value = super(CookBookRecipe, self).save(*args, **kwargs) self.cookbook.get_recipes_count(recache=True) self.cookbook.__class__.objects.get_user_cookbook_items_for_recipe( self.cookbook.owner, self.recipe, recache=True) return ret_value def delete(self, *args, **kwargs): cookbook = self.cookbook recipe = self.recipe super(CookBookRecipe, self).delete(*args, **kwargs) cookbook.get_recipes_count(recache=True) cookbook.__class__.objects.get_user_cookbook_items_for_recipe( cookbook.owner, recipe, recache=True)
class SubstituteIngredient(models.Model): ingredient = CachedForeignKey(Ingredient, verbose_name=_('Ingredient')) substitute = CachedForeignKey(Ingredient, verbose_name=_('Substitute ingredient'), related_name='substitute_ingredients') objects = managers.SubstituteIngredientManager() def __unicode__(self): return force_text( _("%(sub)s is substitute for %(ing)s") % { 'sub': self.substitute, 'ing': self.ingredient, }) class Meta: verbose_name = _('Substitute ingredient') verbose_name_plural = _('Substitute ingredients') def save(self, *args, **kwargs): try: self.clean() except ValidationError, e: raise IntegrityError(e.messages) super(SubstituteIngredient, self).save(*args, **kwargs)
class ShoppingList(models.Model): owner = CachedForeignKey(User) title = models.CharField(_("Title"), max_length=155) note = models.TextField(_("Note"), blank=True) def __unicode__(self): return u"%s: %s" % (_("Shopping list"), self.title) class Meta: verbose_name = _("Shopping list") verbose_name_plural = _("Shopping lists")
class RecipePhoto(models.Model): objects = managers.RecipePhotoManager() recipe = CachedForeignKey(Recipe, verbose_name=_('Recipe')) photo = CachedForeignKey(Photo, verbose_name=_('Photo')) is_visible = models.BooleanField(_('Visible'), default=True) is_checked = models.BooleanField(_('Checked'), default=False) order = models.PositiveSmallIntegerField(_('Order'), db_index=True, blank=True) def __unicode__(self): return u"%d. %s" % (self.order, self.photo) class Meta: unique_together = ( ('recipe', 'photo'), ('recipe', 'order'), ) verbose_name = _('Recipe photo') verbose_name_plural = _('Recipe photos') def save(self, *args, **kwargs): if not self.order: order = RecipePhoto.objects.filter(recipe=self.recipe).values_list( 'order', flat=True).order_by("-order") self.order = order[0] + 1 if order else 1 super(RecipePhoto, self).save(*args, **kwargs) @classmethod def _bump_photos(cls, *args, **kwargs): recipe_id = kwargs.get('instance').recipe_id try: recipe = Recipe.objects.get(pk=recipe_id) except Recipe.DoesNotExist: pass else: recipe.get_photos(recache=True)
class ShoppingListItem(models.Model): shopping_list = CachedForeignKey(ShoppingList) ingredient = CachedForeignKey(Ingredient, verbose_name=_('Ingredient')) amount = models.DecimalField(_('Amount'), max_digits=5, decimal_places=2, null=True, blank=True) unit = models.PositiveSmallIntegerField(_('Unit'), choices=conf.UNIT_CHOICES, null=True, blank=True) note = models.CharField(_('Note'), max_length=255, blank=True) def __unicode__(self): return u"%s %s %s" % (self.ingredient, _("in shopping list"), self.shopping_list.title) class Meta: unique_together = (('shopping_list', 'ingredient'), ) verbose_name = _("Shopping list item") verbose_name_plural = _("Shopping list items")
class WeekMenu(models.Model): day = models.IntegerField(_("Day of the week"), choices=conf.WEEK_DAYS) soup = CachedForeignKey(Recipe, blank=True, null=True, related_name="menu_soup", verbose_name=_('Soup')) meal = CachedForeignKey(Recipe, blank=True, null=True, related_name="menu_meal", verbose_name=_('Meal')) dessert = CachedForeignKey(Recipe, blank=True, null=True, related_name="menu_dessert", verbose_name=_('Dessert')) even_week = models.BooleanField( _("Menu for even week"), default=False, help_text=string_concat( _("Check if this day menu is for even week. Current week is "), _("odd") if date.isocalendar(date.today())[1] % 2 else _("even"), ".")) objects = managers.WeekMenuManager() class Meta: unique_together = (('day', 'even_week'), ) verbose_name = _("Menu of the day") verbose_name_plural = _("Menus of the day") def __unicode__(self): return u"%s week, day %s" % (_("Even") if self.even_week else _("Odd"), self.get_day_display())
class CookBook(models.Model): owner = CachedForeignKey(User) title = models.CharField(_("Title"), max_length=128) slug = models.SlugField(_("Slug"), max_length=128) is_public = models.BooleanField(_("Public"), default=True) recipes = models.ManyToManyField(Recipe, verbose_name=_('Recipes'), through=CookBookRecipe) is_default = models.BooleanField(_("Default cookbook"), default=False) objects = managers.CookBookManager() def __unicode__(self): return u"%s's cookbok: %s" % (self.owner, self.title) class Meta: unique_together = (('owner', 'slug'), ) verbose_name = _('Cookbook') verbose_name_plural = _('Cookbooks') def save(self, *args, **kwargs): self.slug = slugify(self.title) return super(CookBook, self).save(*args, **kwargs) def get_recipes_count(self, recache=False): cache_key = "%s_get_cookbook_recipes" % self.pk recipes_count = cache.get(cache_key) if recipes_count is None or recache: recipes_count = CookBookRecipe.objects.filter( cookbook=self).count() cache.set(cache_key, recipes_count) return recipes_count def get_top_photo(self): recipes = self.recipes.all().order_by('id')[:1] if not recipes: return "" return recipes[0].top_photo @cached_property def top_photo(self): return self.get_top_photo() def get_absolute_url(self): owner = self.owner return reverse('yummy:cookbook_detail', args=(slugify(owner.username), owner.pk, self.slug))
class Ingredient(models.Model): group = CachedForeignKey(IngredientGroup, verbose_name=_('Group'), null=True, blank=True) name = models.CharField(_('Name'), max_length=128) slug = models.SlugField(_('Slug'), max_length=64, unique=True) genitive = models.CharField(_('Genitive'), max_length=128, blank=True) default_unit = models.PositiveSmallIntegerField( choices=conf.UNIT_CHOICES, verbose_name=_('Default unit'), null=True, blank=True) ndb_no = models.IntegerField(_('NDB id number'), blank=True, null=True) is_approved = models.BooleanField(_('Approved'), default=True, db_index=True) objects = managers.IngredientManager() def __unicode__(self): return self.name class Meta: verbose_name = _('Ingredient') verbose_name_plural = _('Ingredients') def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Ingredient, self).save(*args, **kwargs) Ingredient.objects.get_names_list(recache=True) def get_absolute_url(self): return reverse('yummy:ingredient_detail', args=(self.slug, )) @cached_property def substitutes(self): return SubstituteIngredient.objects.get_for_ingredient_cached(self)
class IngredientInRecipeGroup(models.Model): recipe = CachedForeignKey(Recipe, verbose_name=_('Recipe')) title = models.CharField(_('Title'), max_length=128) description = models.TextField(_('Short description'), blank=True) order = models.PositiveSmallIntegerField(_('Order'), db_index=True, blank=True) def __unicode__(self): return u"%s %s" % (self.recipe, self.title) class Meta: verbose_name = _('Ingredients in recipe group') verbose_name_plural = _('Ingredients in recipe groups') def save(self, *args, **kwargs): if not self.order: self.order = IngredientInRecipeGroup.objects.filter( recipe=self.recipe).count() + 1 super(IngredientInRecipeGroup, self).save(*args, **kwargs)
class FlatComment(models.Model): site = SiteForeignKey(default=Site.objects.get_current) content_type = ContentTypeForeignKey() object_id = models.CharField(max_length=255) content_object = CachedGenericForeignKey('content_type', 'object_id') comment = models.TextField() submit_date = models.DateTimeField(default=None) user = CachedForeignKey(User) is_public = models.BooleanField(default=True) app_data = AppDataField() def _comment_list(self, reversed=False): if not hasattr(self, '__comment_list'): self.__comment_list = CommentList(self.content_type, self.object_id, reversed) return self.__comment_list def post(self, request=None): return self._comment_list().post_comment(self, request) def moderate(self, user=None, commit=True): return self._comment_list().moderate_comment(self, user, commit) def get_absolute_url(self, reversed=False): return '%s?p=%d' % (resolver.reverse( self.content_object, 'comments-list'), self._comment_list(reversed).page_index(self.pk)) def delete(self): self.moderate() super(FlatComment, self).delete() def save(self, **kwargs): if self.submit_date is None: self.submit_date = timezone.now() super(FlatComment, self).save(**kwargs)
class RecipeRecommendation(models.Model): objects = managers.RecipeRecommendationManager() day_from = models.DateField( _("Show from day"), help_text=_("Recipe will show itself starting this day")) day_to = models.DateField( _("Show until day (inclusive)"), blank=True, null=True, help_text=_("Recipe shown until this day. This field is not required. " "The longer is recipe shown, the lower priority it has.")) recipe = CachedForeignKey(Recipe) def __unicode__(self): return u"'%s', %s - %s" % (self.recipe, self.day_from, (self.day_to or _('until forever'))) class Meta: verbose_name = _("Recipe recommendation") verbose_name_plural = _("Recipe recommendations") def clean(self): if not self.recipe.is_approved: raise ValidationError( _("You can save recommendation only with approved recipe")) if self.day_to and self.day_to < self.day_from: raise ValidationError(_("Invalid chronology of border dates")) def save(self, *args, **kwargs): try: self.clean() except ValidationError, e: raise IntegrityError(e.messages) super(RecipeRecommendation, self).save(*args, **kwargs)
class IngredientInRecipe(models.Model): recipe = CachedForeignKey(Recipe, verbose_name=_('Recipe')) group = CachedForeignKey(IngredientInRecipeGroup, verbose_name=_('Group'), null=True, blank=True) ingredient = CachedForeignKey(Ingredient, verbose_name=_('Ingredient')) amount = models.DecimalField(_('Amount'), max_digits=5, decimal_places=2, null=True, blank=True) unit = models.PositiveSmallIntegerField(_('Unit'), choices=conf.UNIT_CHOICES, null=True, blank=True) order = models.PositiveSmallIntegerField(_('Order'), db_index=True, blank=True) note = models.CharField(_('Note'), max_length=255, blank=True) def __unicode__(self): return u"%s - %s" % (self.ingredient, self.recipe) class Meta: verbose_name = _('Ingredient in recipe') verbose_name_plural = _('Ingredients in recipe') def save(self, *args, **kwargs): if not self.order: self.order = IngredientInRecipe.objects.filter( recipe=self.recipe).count() + 1 super(IngredientInRecipe, self).save(*args, **kwargs) @property def inflect_unit(self): def _get_magic_unit(): if conf.ALLOW_MAGIC_UNITS_TRANSFORM: res = f(5) res = ''.join([res[0:-1], slugify(res[-1])]) else: res = f(1) return res if self.unit is None or self.amount is None: return "" f, amount_for_decimal = conf.DICT_UNITS[self.unit] i = int(self.amount) # if decimal amount is equal to int(amount) return if self.amount == i: return f(i) # else construct magic unit num_parts = str(self.amount).split(".") if len(num_parts) == 1: res = f(self.amount) else: res = amount_for_decimal == 'm' and _get_magic_unit() or f( amount_for_decimal) return res
class Recipe(models.Model): objects = managers.RecipeManager() title = models.CharField(_('Title'), max_length=128) slug = models.SlugField(_('Slug'), max_length=64, unique=True) category = CachedForeignKey(Category, verbose_name=_("Category")) description = models.TextField(_('Short description'), blank=True) preparation = models.TextField(_('Preparation')) hint = models.TextField(_('Hint'), blank=True) cooking_type = CachedForeignKey(CookingType, verbose_name=_('Cooking type'), blank=True, null=True) cuisines = models.ManyToManyField(Cuisine, verbose_name=_('Cuisines'), blank=True) servings = models.PositiveSmallIntegerField( _('No. of servings'), choices=getattr(settings, 'YUMMY_SERVINGS_CHOICES', None), blank=True, null=True) price = models.SmallIntegerField(_('Price'), choices=conf.PRICING_CHOICES, default=3, db_index=True, null=True, blank=True) difficulty = models.PositiveSmallIntegerField( _('Preparation difficulty'), choices=conf.DIFFICULTY_CHOICES, default=3, db_index=True, null=True, blank=True) preparation_time = models.PositiveSmallIntegerField( _('Preparation time (min)'), blank=True, null=True) caloric_value = models.PositiveIntegerField(_('Caloric value'), blank=True, null=True) owner = CachedForeignKey(User, verbose_name=_('User')) is_approved = models.BooleanField(_('Approved'), default=False, db_index=True) is_public = models.BooleanField(_('Public'), default=True) is_checked = models.BooleanField(_("Is checked"), default=False) created = models.DateTimeField(editable=False, db_index=True) updated = models.DateTimeField(editable=False) def __unicode__(self): return self.title def save(self, **kwargs): self.updated = now() if not self.id: self.created = self.updated if not self.slug: self.slug = slugify(self.title) super(Recipe, self).save(**kwargs) self.groupped_ingredients(recache=True) self.category.get_recipes_count(recache=True) class Meta: verbose_name = _('Recipe') verbose_name_plural = _('Recipes') permissions = (("approve_recipe", "Can approve recipe"), ) def get_photos(self, recache=False): cache_key = '%s_recipe_photos' % self.pk cached_photos = cache.get(cache_key) if cached_photos is None or recache: cached_photos = [] qs = self.recipephoto_set.visible().select_related( 'photo').order_by('order') for one in qs: if one.photo.owner_id == self.owner.pk: cached_photos.insert(0, one.photo) else: cached_photos.append(one.photo) cache.set(cache_key, cached_photos) return cached_photos def get_top_photo(self): """ Get to photo for recipe. Prefer photo from recipe's owner, if available. If recipe doesn't have any photo, try to get photo for recipe's category :return: photo for recipe :rtype: Photo """ photos = self.get_photos() if photos: return photos[0] else: return self.category.photo_hierarchic @cached_property def top_photo(self): return self.get_top_photo() @recached_method_to_mem def groupped_ingredients(self, recache=False): """ order items by group's priority if items are not in any group, set them into anonymous group \ which has highest priority (lowest number) to keep priorities while listing, conversion to list is needed :param recache: force recache :type recache: bool :return: list of groups w/ items: (group, {prioriy:1, items:[]} :rtype: list """ cache_key = '%s_groupped_ingredients' % self.pk groups = cache.get(cache_key) if groups is None or recache: qs = IngredientInRecipe.objects.filter( recipe=self).select_related('ingredient').order_by( 'group__order', 'order') tmp_groups = {} for one in qs: group_index = one.group.title if one.group else '__nogroup__' group_priority = one.group.order if one.group else 0 tmp_groups.setdefault( group_index, dict(items=[], priority=group_priority))['items'].append(one) groups = sorted(tmp_groups.items(), key=lambda x: x[1].get('priority')) cache.set(cache_key, groups) return groups def get_absolute_url(self): return reverse('yummy:recipe_detail', args=( self.category.path, self.slug, self.pk, ))
class Category(models.Model): objects = managers.CategoryManager() parent = CachedForeignKey('self', null=True, blank=True) title = models.CharField(_('Title'), max_length=128) slug = models.SlugField(_('Slug'), max_length=64) # fake recipe photo photo = CachedForeignKey(Photo, verbose_name=_('Photo'), null=True, blank=True) path = models.CharField(max_length=255, editable=False, unique=True) description = models.TextField(_('Description'), blank=True) class Meta: verbose_name = _('Category') verbose_name_plural = _('Categories') ordering = ('path', ) def __unicode__(self): return self.title def get_root_ancestor(self): if self.parent is not None: return self.parent.get_root_ancestor() return self @property def chained_title(self): if self.parent: return "%s / %s" % (self.parent.chained_title, self.title) return self.title def get_absolute_url(self): return reverse('yummy:category_detail', args=(self.path, )) @property def is_root_category(self): return self.parent is None def is_ancestor_of(self, category=None): if category is None or category.is_root_category: return False if category.parent == self: return True return self.is_ancestor_of(category.parent) def get_children(self, recache=False): cache_key = "%s_direct_descendants" % self.pk cached_cats = cache.get(cache_key) if cached_cats is None or recache: cached_cats = list(self.__class__.objects.filter(parent=self)) cache.set(cache_key, cached_cats) return cached_cats def get_descendants(self, recache=False): cats = [] for child_category in self.get_children(recache): cats += [child_category] cats += child_category.get_descendants() return cats @property def level(self): return len(self.path.split('/')) def path_is_unique(self): if self.parent: path = '%s/%s' % (self.parent.path, self.slug) else: path = self.slug qs = self.__class__.objects.filter(path=path) if self.pk: qs = qs.exclude(pk=self.pk) return not bool(qs.count()) def clean(self): if self == self.parent: raise ValidationError( _('Parent category must be different than child.')) if self.is_ancestor_of(self.parent): raise ValidationError( _('A parent can\'t be a descendant of this category.')) if not self.path_is_unique(): raise ValidationError( _('Path is not unique, change category title or slug.')) def save(self, **kwargs): """Override save() to construct path based on the category's parent.""" old_path = self.path if not self.slug: self.slug = slugify(self.title) if self.parent: if self == self.parent or self.is_ancestor_of(self.parent): raise IntegrityError( 'Bad category structure. Check category parent.') self.path = '%s/%s' % (self.parent.path, self.slug) else: self.path = self.slug super(Category, self).save(**kwargs) if old_path != self.path: if self.parent: self.parent.get_descendants(recache=True) # update descendants' path for cat in self.get_descendants(): cat.save(force_update=True) @property def photo_hierarchic(self): if self.photo: return self.photo if self.parent: return self.parent.photo_hierarchic return "" def get_recipes_count(self, recache=False): cache_key = '%s_subcats_recipes_count' % self.pk recipes_count = cache.get(cache_key) if recipes_count is None or recache: recipes_count = Recipe.objects.filter(is_public=True, is_approved=True, category=self).count() children = Category.objects.filter(parent=self) for one in children: recipes_count += one.get_recipes_count() cache.set(cache_key, recipes_count) return recipes_count
class FlatComment(models.Model): site = SiteForeignKey(default=Site.objects.get_current) content_type = ContentTypeForeignKey() object_id = models.CharField(max_length=255) content_object = CachedGenericForeignKey('content_type', 'object_id') comment = models.TextField() submit_date = models.DateTimeField(default=None) user = CachedForeignKey(User) is_public = models.BooleanField(default=True) app_data = AppDataField() def _comment_list(self, reversed=False): if not hasattr(self, '__comment_list'): self.__comment_list = CommentList(self.content_type, self.object_id, reversed) return self.__comment_list def post(self, request=None): return self._comment_list().post_comment(self, request) def moderate(self, user=None, commit=True): return self._comment_list().moderate_comment(self, user, commit) def get_absolute_url(self, reversed=False): return '%s?p=%d' % ( resolver.reverse(self.content_object, 'comments-list'), self._comment_list(reversed).page_index(self.pk) ) def delete(self): self.moderate() super(FlatComment, self).delete() def save(self, **kwargs): if self.submit_date is None: self.submit_date = timezone.now() super(FlatComment, self).save(**kwargs) def has_edit_timer(self): '''has_edit_timer() -> bool ''' return EDIT_TIMER_ENABLED def is_edit_timer_expired(self): '''is_edit_timer_expired() -> bool Check whether the comment is still within the allowed edit time from the creation time. ''' age = datetime.now(self.submit_date.tzinfo) - self.submit_date if age >= timedelta(minutes=EDIT_TIMER_MINUTES): return True return False def get_remaining_edit_time(self): '''get_remaining_edit_time() -> str Returns the remaining edit time from comment creation. The returned string is formatted as HH:MM:SS, e.g. 0:01:23 ''' age = datetime.now(self.submit_date.tzinfo) - self.submit_date edit_time = timedelta(minutes=EDIT_TIMER_MINUTES) if age >= edit_time: return '0:00:00' seconds = edit_time.total_seconds() - age.total_seconds() remaining = timedelta(seconds=seconds) text = str(remaining) text = text.split('.')[0] return text