class ExerciseBase(AbstractSubmissionModel, AbstractLicenseModel, models.Model): """ Model for an exercise base """ objects = SubmissionManager() """Custom manager""" uuid = models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID') """Globally unique ID, to identify the base across installations""" category = models.ForeignKey(ExerciseCategory, verbose_name=_('Category'), on_delete=models.CASCADE) muscles = models.ManyToManyField(Muscle, blank=True, verbose_name=_('Primary muscles')) """Main muscles trained by the exercise""" muscles_secondary = models.ManyToManyField( Muscle, verbose_name=_('Secondary muscles'), related_name='secondary_muscles_base', blank=True) """Secondary muscles trained by the exercise""" equipment = models.ManyToManyField(Equipment, verbose_name=_('Equipment'), blank=True) """Equipment needed by this exercise""" variations = models.ForeignKey(Variation, verbose_name=_('Variations'), on_delete=models.CASCADE, null=True, blank=True) """Variations of this exercise""" # # Own methods # @property def get_languages(self): """ Returns the languages from the exercises that use this base """ return [exercise.language for exercise in self.exercises.all()]
class Ingredient(AbstractSubmissionModel, AbstractLicenseModel, models.Model): """ An ingredient, with some approximate nutrition values """ objects = SubmissionManager() """Custom manager""" ENERGY_APPROXIMATION = 15 """ How much the calculated energy from protein, etc. can deviate from the energy amount given (in percent). """ # Metaclass to set some other properties class Meta: ordering = [ "name", ] # Meta data language = models.ForeignKey( Language, verbose_name=_('Language'), editable=False, on_delete=models.CASCADE, ) creation_date = models.DateField(_('Date'), auto_now_add=True) update_date = models.DateField( _('Date'), auto_now=True, blank=True, editable=False, ) # Product infos name = models.CharField( max_length=200, verbose_name=_('Name'), validators=[MinLengthValidator(3)], ) energy = models.IntegerField(verbose_name=_('Energy'), help_text=_('In kcal per 100g')) protein = models.DecimalField( decimal_places=3, max_digits=6, verbose_name=_('Protein'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) carbohydrates = models.DecimalField( decimal_places=3, max_digits=6, verbose_name=_('Carbohydrates'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) carbohydrates_sugar = models.DecimalField( decimal_places=3, max_digits=6, blank=True, null=True, verbose_name=_('Sugar content in carbohydrates'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) fat = models.DecimalField( decimal_places=3, max_digits=6, verbose_name=_('Fat'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) fat_saturated = models.DecimalField( decimal_places=3, max_digits=6, blank=True, null=True, verbose_name=_('Saturated fat content in fats'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) fibres = models.DecimalField( decimal_places=3, max_digits=6, blank=True, null=True, verbose_name=_('Fibres'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) sodium = models.DecimalField( decimal_places=3, max_digits=6, blank=True, null=True, verbose_name=_('Sodium'), help_text=_('In g per 100g of product'), validators=[MinValueValidator(0), MaxValueValidator(100)], ) code = models.CharField( max_length=200, null=True, blank=True, db_index=True, ) """Internal ID of the source database, e.g. a barcode or similar""" source_name = models.CharField( max_length=200, null=True, blank=True, ) """Name of the source, such as Open Food Facts""" source_url = models.URLField( verbose_name=_('Link'), help_text=_('Link to product'), blank=True, null=True, ) """URL of the product at the source""" last_imported = models.DateTimeField( _('Date'), auto_now_add=True, null=True, blank=True, ) common_name = models.CharField( max_length=200, null=True, blank=True, ) category = models.ForeignKey( IngredientCategory, verbose_name=_('Category'), on_delete=models.CASCADE, null=True, blank=True, ) brand = models.CharField( max_length=200, verbose_name=_('Brand name of product'), null=True, blank=True, ) # # Django methods # def get_absolute_url(self): """ Returns the canonical URL to view this object. Since some names consist of only non-ascii characters (e.g. 감자깡), the resulting slug would be empty and no URL would match. In that case, use the regular URL with only the ID. """ slug = slugify(self.name) if not slug: return reverse('nutrition:ingredient:view', kwargs={'id': self.id}) else: return reverse('nutrition:ingredient:view', kwargs={ 'id': self.id, 'slug': slug }) def clean(self): """ Do a very broad sanity check on the nutritional values according to the following rules: - 1g of protein: 4kcal - 1g of carbohydrates: 4kcal - 1g of fat: 9kcal The sum is then compared to the given total energy, with ENERGY_APPROXIMATION percent tolerance. """ # Note: calculations in 100 grams, to save us the '/100' everywhere energy_protein = 0 if self.protein: energy_protein = self.protein * ENERGY_FACTOR['protein']['kg'] energy_carbohydrates = 0 if self.carbohydrates: energy_carbohydrates = self.carbohydrates * ENERGY_FACTOR[ 'carbohydrates']['kg'] energy_fat = 0 if self.fat: # TODO: for some reason, during the tests the fat value is not # converted to decimal (django 1.9) energy_fat = Decimal(self.fat * ENERGY_FACTOR['fat']['kg']) energy_calculated = energy_protein + energy_carbohydrates + energy_fat # Compare the values, but be generous if self.energy: energy_upper = self.energy * ( 1 + (self.ENERGY_APPROXIMATION / Decimal(100.0))) energy_lower = self.energy * ( 1 - (self.ENERGY_APPROXIMATION / Decimal(100.0))) if not ((energy_upper > energy_calculated) and (energy_calculated > energy_lower)): raise ValidationError( _('The total energy ({energy}kcal) is not the approximate sum of the ' 'energy provided by protein, carbohydrates and fat ({energy_calculated}kcal ' '+/-{energy_approx}%)'.format( energy=self.energy, energy_calculated=energy_calculated, energy_approx=self.ENERGY_APPROXIMATION))) def save(self, *args, **kwargs): """ Reset the cache """ super(Ingredient, self).save(*args, **kwargs) cache.delete(cache_mapper.get_ingredient_key(self.id)) def __str__(self): """ Return a more human-readable representation """ return self.name def __eq__(self, other): """ Compare ingredients based on their values, not like django on their PKs """ logger.debug( 'Overwritten behaviour: comparing ingredients on values, not PK.') equal = True if isinstance(other, self.__class__): for i in self._meta.fields: if (hasattr(self, i.name) and hasattr(other, i.name) and (getattr(self, i.name, None) != getattr( other, i.name, None))): equal = False else: equal = False return equal def __hash__(self): """ Define a hash function This is rather unnecessary, but it seems that newer versions of django have a problem when the __eq__ function is implemented, but not the __hash__ one. Returning hash(pk) is also django's default. :return: hash(pk) """ return hash(self.pk) # # Own methods # def compare_with_database(self): """ Compares the current ingredient with the version saved in the database. If the current object has no PK, returns false """ if not self.pk: return False ingredient = Ingredient.objects.get(pk=self.pk) if self != ingredient: return False else: return True def send_email(self, request): """ Sends an email after being successfully added to the database (for user submitted ingredients only) """ try: user = User.objects.get(username=self.license_author) except User.DoesNotExist: return if self.license_author and user.email: translation.activate( user.userprofile.notification_language.short_name) url = request.build_absolute_uri(self.get_absolute_url()) subject = _( 'Ingredient was successfully added to the general database') context = { 'ingredient': self.name, 'url': url, 'site': Site.objects.get_current().domain } message = render_to_string('ingredient/email_new.tpl', context) mail.send_mail(subject, message, settings.WGER_SETTINGS['EMAIL_FROM'], [user.email], fail_silently=True) def set_author(self, request): if request.user.has_perm('nutrition.add_ingredient'): self.status = Ingredient.STATUS_ACCEPTED if not self.license_author: self.license_author = request.get_host().split(':')[0] else: if not self.license_author: self.license_author = request.user.username # Send email to administrator subject = _('New user submitted ingredient') message = _( """The user {0} submitted a new ingredient "{1}".""".format( request.user.username, self.name)) mail.mail_admins(subject, message, fail_silently=True) def get_owner_object(self): """ Ingredient has no owner information """ return False @property def energy_kilojoule(self): """ returns kilojoules for current ingredient, 0 if energy is uninitialized """ if self.energy: return Decimal(self.energy * 4.184).quantize(TWOPLACES) else: return 0
class ExerciseImage(AbstractSubmissionModel, AbstractLicenseModel, models.Model): """ Model for an exercise image """ objects = SubmissionManager() """Custom manager""" exercise = models.ForeignKey(Exercise, verbose_name=_('Exercise'), on_delete=models.CASCADE) """The exercise the image belongs to""" image = models.ImageField( verbose_name=_('Image'), help_text=_('Only PNG and JPEG formats are supported'), upload_to=exercise_image_upload_dir) """Uploaded image""" is_main = models.BooleanField( verbose_name=_('Main picture'), default=False, help_text=_("Tick the box if you want to set this image as the " "main one for the exercise (will be shown e.g. in " "the search). The first image is automatically " "marked by the system.")) """A flag indicating whether the image is the exercise's main image""" class Meta: """ Set default ordering """ ordering = ['-is_main', 'id'] base_manager_name = 'objects' def save(self, *args, **kwargs): """ Only one image can be marked as main picture at a time """ if self.is_main: ExerciseImage.objects.filter(exercise=self.exercise).update( is_main=False) self.is_main = True else: if ExerciseImage.objects.accepted().filter(exercise=self.exercise).count() == 0 \ or not ExerciseImage.objects.accepted() \ .filter(exercise=self.exercise, is_main=True)\ .count(): self.is_main = True # # Reset all cached infos # for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('exercise-overview-mobile', language.id) delete_template_fragment_cache('equipment-overview', language.id) # And go on super(ExerciseImage, self).save(*args, **kwargs) def delete(self, *args, **kwargs): """ Reset all cached infos """ super(ExerciseImage, self).delete(*args, **kwargs) for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('exercise-overview-mobile', language.id) delete_template_fragment_cache('equipment-overview', language.id) # Make sure there is always a main image if not ExerciseImage.objects.accepted() \ .filter(exercise=self.exercise, is_main=True).count() \ and ExerciseImage.objects.accepted() \ .filter(exercise=self.exercise) \ .filter(is_main=False) \ .count(): image = ExerciseImage.objects.accepted() \ .filter(exercise=self.exercise, is_main=False)[0] image.is_main = True image.save() def get_owner_object(self): """ Image has no owner information """ return False def set_author(self, request): """ Set author and status This is only used when creating images (via web or API) """ if request.user.has_perm('exercises.add_exerciseimage'): self.status = self.STATUS_ACCEPTED if not self.license_author: self.license_author = request.get_host().split(':')[0] else: if not self.license_author: self.license_author = request.user.username subject = _('New user submitted image') message = _( 'The user {0} submitted a new image "{1}" for exercise {2}.' ).format(request.user.username, self.name, self.exercise) mail.mail_admins(str(subject), str(message), fail_silently=True)
class Exercise(AbstractSubmissionModel, AbstractLicenseModel, models.Model): """ Model for an exercise """ objects = SubmissionManager() """Custom manager""" category = models.ForeignKey(ExerciseCategory, verbose_name=_('Category'), on_delete=models.CASCADE) description = models.TextField(max_length=2000, verbose_name=_('Description'), validators=[MinLengthValidator(40)]) """Description on how to perform the exercise""" name = models.CharField(max_length=200, verbose_name=_('Name')) """The exercise's name, with correct uppercase""" name_original = models.CharField(max_length=200, verbose_name=_('Name'), default='') """The exercise's name, as entered by the user""" muscles = models.ManyToManyField(Muscle, blank=True, verbose_name=_('Primary muscles')) """Main muscles trained by the exercise""" muscles_secondary = models.ManyToManyField( Muscle, verbose_name=_('Secondary muscles'), related_name='secondary_muscles', blank=True) """Secondary muscles trained by the exercise""" equipment = models.ManyToManyField(Equipment, verbose_name=_('Equipment'), blank=True) """Equipment needed by this exercise""" creation_date = models.DateField(_('Date'), auto_now_add=True, null=True, blank=True) """The submission date""" language = models.ForeignKey(Language, verbose_name=_('Language'), on_delete=models.CASCADE) """The exercise's language""" uuid = models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID') """ Globally unique ID, to identify the exercise across installations """ # # Django methods # class Meta: base_manager_name = 'objects' ordering = [ "name", ] def get_absolute_url(self): """ Returns the canonical URL to view an exercise """ return reverse('exercise:exercise:view', kwargs={ 'id': self.id, 'slug': slugify(self.name) }) def save(self, *args, **kwargs): """ Reset all cached infos """ self.name = smart_capitalize(self.name_original) super(Exercise, self).save(*args, **kwargs) # Cached template fragments for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('equipment-overview', language.id) # Cached workouts for set in self.set_set.all(): reset_workout_canonical_form(set.exerciseday.training_id) def delete(self, *args, **kwargs): """ Reset all cached infos """ # Cached template fragments for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('equipment-overview', language.id) # Cached workouts for set in self.set_set.all(): reset_workout_canonical_form(set.exerciseday.training.pk) super(Exercise, self).delete(*args, **kwargs) def __str__(self): """ Return a more human-readable representation """ return self.name # # Own methods # @property def main_image(self): """ Return the main image for the exercise or None if nothing is found """ return self.exerciseimage_set.accepted().filter(is_main=True).first() @property def description_clean(self): """ Return the exercise description with all markup removed """ return bleach.clean(self.description, strip=True) def get_owner_object(self): """ Exercise has no owner information """ return False def send_email(self, request): """ Sends an email after being successfully added to the database (for user submitted exercises only) """ try: user = User.objects.get(username=self.license_author) except User.DoesNotExist: return if self.license_author and user.email: translation.activate( user.userprofile.notification_language.short_name) url = request.build_absolute_uri(self.get_absolute_url()) subject = _( 'Exercise was successfully added to the general database') context = { 'exercise': self.name, 'url': url, 'site': Site.objects.get_current().domain } message = render_to_string('exercise/email_new.tpl', context) mail.send_mail(subject, message, settings.WGER_SETTINGS['EMAIL_FROM'], [user.email], fail_silently=True) def set_author(self, request): """ Set author and status This is only used when creating exercises (via web or API) """ if request.user.has_perm('exercises.add_exercise'): self.status = self.STATUS_ACCEPTED if not self.license_author: self.license_author = request.get_host().split(':')[0] else: if not self.license_author: self.license_author = request.user.username subject = _('New user submitted exercise') message = _('The user {0} submitted a new exercise "{1}".').format( request.user.username, self.name) mail.mail_admins(str(subject), str(message), fail_silently=True)
class Exercise(AbstractSubmissionModel, AbstractLicenseModel, models.Model): ''' Model for an exercise ''' objects = SubmissionManager() '''Custom manager''' category = models.ForeignKey(ExerciseCategory, verbose_name=_('Category')) description = models.TextField(max_length=2000, verbose_name=_('Description'), validators=[MinLengthValidator(40)]) '''Description on how to perform the exercise''' name = models.CharField(max_length=200, verbose_name=_('Name')) muscles = models.ManyToManyField(Muscle, blank=True, verbose_name=_('Primary muscles')) '''Main muscles trained by the exercise''' muscles_secondary = models.ManyToManyField( Muscle, verbose_name=_('Secondary muscles'), related_name='secondary_muscles', blank=True) '''Secondary muscles trained by the exercise''' equipment = models.ManyToManyField(Equipment, verbose_name=_('Equipment'), blank=True) '''Equipment needed by this exercise''' creation_date = models.DateField(_('Date'), auto_now_add=True, null=True, blank=True) '''The submission date''' language = models.ForeignKey(Language, verbose_name=_('Language')) '''The exercise's language''' uuid = models.CharField(verbose_name='UUID', max_length=36, editable=False, default=uuid.uuid4) ''' Globally unique ID, to identify the exercise across installations ''' # # Django methods # class Meta: ordering = [ "name", ] def get_absolute_url(self): ''' Returns the canonical URL to view an exercise ''' return reverse('exercise:exercise:view', kwargs={ 'id': self.id, 'slug': slugify(self.name) }) def save(self, *args, **kwargs): ''' Reset all cached infos ''' super(Exercise, self).save(*args, **kwargs) # Cached objects cache.delete(cache_mapper.get_exercise_key(self)) cache.delete(cache_mapper.get_exercise_muscle_bg_key(self)) # Cached template fragments for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('exercise-overview-mobile', language.id) delete_template_fragment_cache('exercise-detail-header', self.id, language.id) delete_template_fragment_cache('exercise-detail-muscles', self.id, language.id) delete_template_fragment_cache('equipment-overview', language.id) # Cached workouts for set in self.set_set.all(): reset_workout_canonical_form(set.exerciseday.training_id) def delete(self, *args, **kwargs): ''' Reset all cached infos ''' # Cached objects cache.delete(cache_mapper.get_exercise_key(self)) cache.delete(cache_mapper.get_exercise_muscle_bg_key(self)) # Cached template fragments for language in Language.objects.all(): delete_template_fragment_cache('muscle-overview', language.id) delete_template_fragment_cache('exercise-overview', language.id) delete_template_fragment_cache('exercise-overview-mobile', language.id) delete_template_fragment_cache('exercise-detail-header', self.id, language.id) delete_template_fragment_cache('exercise-detail-muscles', self.id, language.id) delete_template_fragment_cache('equipment-overview', language.id) # Cached workouts for set in self.set_set.all(): reset_workout_canonical_form(set.exerciseday.training.pk) super(Exercise, self).delete(*args, **kwargs) def __str__(self): ''' Return a more human-readable representation ''' return self.name # # Own methods # @property def main_image(self): ''' Return the main image for the exercise or None if nothing is found ''' return self.exerciseimage_set.accepted().filter(is_main=True).first() @property def description_clean(self): ''' Return the exercise description with all markup removed ''' return bleach.clean(self.description, strip=True) def get_owner_object(self): ''' Exercise has no owner information ''' return False def send_email(self, request): ''' Sends an email after being successfully added to the database (for user submitted exercises only) ''' try: user = User.objects.get(username=self.license_author) except User.DoesNotExist: return if self.license_author and user.email: translation.activate( user.userprofile.notification_language.short_name) url = request.build_absolute_uri(self.get_absolute_url()) subject = _( 'Exercise was successfully added to the general database') context = {'exercise': self.name, 'url': url} message = render_to_string('exercise/email_new.html', context) mail.send_mail(subject, message, EMAIL_FROM, [user.email], fail_silently=True) def set_author(self, request): ''' Set author and status This is only used when creating exercises (via web or API) ''' if request.user.has_perm('exercises.add_exercise'): self.status = self.STATUS_ACCEPTED if not self.license_author: self.license_author = request.get_host().split(':')[0] else: if not self.license_author: self.license_author = request.user.username subject = _('New user submitted exercise') message = _( u'The user {0} submitted a new exercise "{1}".').format( request.user.username, self.name) mail.mail_admins(six.text_type(subject), six.text_type(message), fail_silently=True)