<commit_before># -*- coding: utf-8 -*-

# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Workout Manager.  If not, see <http://www.gnu.org/licenses/>.

import logging

from django.db import models
from django.template.defaultfilters import slugify  # django.utils.text.slugify in django 1.5!
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.urlresolvers import reverse
from django.core import mail
from django.core.cache import cache
from django.db.models.signals import pre_save
from django.db.models.signals import post_delete

from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.signals import saved_file
from easy_thumbnails.signal_handlers import generate_aliases_global

from wger.utils.constants import EMAIL_FROM
from wger.utils.cache import delete_template_fragment_cache
from wger.utils.cache import cache_mapper

logger = logging.getLogger('wger.custom')

class Language(models.Model):
    '''
    Language of an item (exercise, workout, etc.)
    '''

    #e.g. 'de'
    short_name = models.CharField(max_length=2,
                                  verbose_name=_('Language short name'))

    #e.g. 'Deutsch'
    full_name = models.CharField(max_length=30,
                                 verbose_name=_('Language full name'))

    #
    # Django methods
    #
    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return u"{0} ({1})".format(self.full_name, self.short_name)

    def get_absolute_url(self):
        '''
        Returns the canonical URL to view a language
        '''
        return reverse('config:language-view', kwargs={'pk': self.id})

    #
    # Own methods
    #
    def get_owner_object(self):
        '''
        Muscle has no owner information
        '''
        return False

class Muscle(models.Model):
    '''
    Muscle an exercise works out
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('Name'),
                            help_text=_('In latin, e.g. "Pectoralis major"'))

    # Whether to use the front or the back image for background
    is_front = models.BooleanField(default=1)

    # Metaclass to set some other properties
    class Meta:
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Muscle has no owner information
        '''
        return False

class Equipment(models.Model):
    '''
    Equipment used or needed by an exercise
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('Name'))

    class Meta:
        '''
        Set default ordering
        '''
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Equipment has no owner information
        '''
        return False

class ExerciseCategory(models.Model):
    '''
    Model for an exercise category
    '''
    name = models.CharField(max_length=100,
                            verbose_name=_('Name'),)

    # Metaclass to set some other properties
    class Meta:
        verbose_name_plural = _("Exercise Categories")
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Category has no owner information
        '''
        return False

class Exercise(models.Model):
    '''
    Model for an exercise
    '''

    EXERCISE_STATUS_PENDING = '1'
    EXERCISE_STATUS_ACCEPTED = '2'
    EXERCISE_STATUS_DECLINED = '3'
    EXERCISE_STATUS_ADMIN = '4'
    EXERCISE_STATUS_SYSTEM = '5'

    EXERCISE_STATUS = (
        (EXERCISE_STATUS_PENDING, _('Pending')),
        (EXERCISE_STATUS_ACCEPTED, _('Accepted')),
        (EXERCISE_STATUS_DECLINED, _('Declined')),
        (EXERCISE_STATUS_ADMIN, _('Submitted by administrator')),
        (EXERCISE_STATUS_SYSTEM, _('System exercise')),
    )

    EXERCISE_STATUS_OK = (EXERCISE_STATUS_ACCEPTED,
                          EXERCISE_STATUS_ADMIN,
                          EXERCISE_STATUS_SYSTEM)

    category = models.ForeignKey(ExerciseCategory,
                                 verbose_name=_('Category'))
    description = models.TextField(max_length=2000,
                                   verbose_name=_('Description'))
    '''Description on how to perform the exercise'''

    name = models.CharField(max_length=200,
                            verbose_name=_('Name'))

    muscles = models.ManyToManyField(Muscle,
                                     verbose_name=_('Primary muscles'),
                                     )
    '''Main muscles trained by the exercise'''

    muscles_secondary = models.ManyToManyField(Muscle,
                                               verbose_name=_('Secondary muscles'),
                                               related_name='secondary_muscles',
                                               null=True,
                                               blank=True
                                               )
    '''Secondary muscles trained by the exercise'''

    equipment = models.ManyToManyField(Equipment,
                                       verbose_name=_('Equipment'),
                                       null=True,
                                       blank=True)
    '''Equipment needed by this exercise'''

    # Non-editable fields
    user = models.ForeignKey(User, verbose_name=_('User'), null=True, blank=True)
    '''The user that submitted the exercise'''

    status = models.CharField(max_length=2,
                              choices=EXERCISE_STATUS,
                              default=EXERCISE_STATUS_PENDING,
                              editable=False)
    '''Status, e.g. accepted or declined'''

    creation_date = models.DateField(_('Date'), auto_now_add=True, null=True, blank=True)
    '''The submission date'''

    language = models.ForeignKey(Language,
                                 verbose_name=_('Language'),
                                 editable=False)
    '''The exercise's language'''

    #
    # Django methods
    #

    # Metaclass to set some other properties
    class Meta:
        ordering = ["name", ]

    def get_absolute_url(self):
        '''
        Returns the canonical URL to view an exercise
        '''
        return reverse('wger.exercises.views.exercises.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
        delete_template_fragment_cache('muscle-overview', self.language_id)
        delete_template_fragment_cache('exercise-overview', self.language_id)
        delete_template_fragment_cache('exercise-detail-header', self.id, self.language_id)
        delete_template_fragment_cache('exercise-detail-muscles', self.id, self.language_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
        delete_template_fragment_cache('muscle-overview', self.language_id)
        delete_template_fragment_cache('exercise-overview', self.language_id)
        delete_template_fragment_cache('exercise-detail-header', self.language_id)
        delete_template_fragment_cache('exercise-detail-muscles', self.language_id)

        super(Exercise, self).delete(*args, **kwargs)

    def __unicode__(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
        '''
        has_image = self.exerciseimage_set.exists()
        image = None
        if has_image:
            image = self.exerciseimage_set.filter(is_main=True)[0]
        return image

    def get_owner_object(self):
        '''
        Exercise has no owner information
        '''
        return False

    def send_email(self, request):
        '''
        Sends an email after being sucessfully added to the database (for user
        submitted exercises only)
        '''
        if self.user and self.user.email:
            url = request.build_absolute_uri(self.get_absolute_url())
            subject = _('Exercise was sucessfully added to the general database')
            message = (ugettext("Your exercise '{0}' was successfully added to the general"
                       "database.\n\n"
                       "It is now available on the exercise and muscle overview and can be\n"
                       "added to workouts. You can access it on this address:\n"
                       "{1}\n\n").format(self.name, url) +
                       ugettext("Thank you for contributing and making this site better!\n"
                                "   the wger.de team"))
            mail.send_mail(subject,
                           message,
                           EMAIL_FROM,
                           [self.user.email],
                           fail_silently=True)

def exercise_image_upload_dir(instance, filename):
    '''
    Returns the upload target for exercise images
    '''
    return "exercise-images/{0}/{1}".format(instance.exercise.id, filename)

class ExerciseImage(models.Model):
    '''
    Model for an exercise image
    '''

    IMAGE_LICENSE_CC_BY_SA_3 = '1'
    IMAGE_LICENSE_PUBLIC_DOMAIN = '2'

    IMAGE_LICENSE = (
        (IMAGE_LICENSE_CC_BY_SA_3, 'CC-BY-SA 3.0'),
        (IMAGE_LICENSE_PUBLIC_DOMAIN, _('Public domain')),
    )

    exercise = models.ForeignKey(Exercise,
                                 verbose_name=_('Exercise'),
                                 editable=False)
    '''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'''

    license = models.CharField(verbose_name=_('License'),
                               max_length=2,
                               choices=IMAGE_LICENSE,
                               default=IMAGE_LICENSE_CC_BY_SA_3)
    '''The license the image is released under'''

    license_author = models.CharField(verbose_name=_('Author'),
                                      max_length=50,
                                      blank=True,
                                      null=True,
                                      help_text=_('If you are not the author of the image, enter '
                                                  'the name here. This is needed for some licenses '
                                                  'e.g. the CC-BY-SA.'))
    '''The author of the image if it is not the uploader'''

    is_main = models.BooleanField(verbose_name=_('Is main picture'),
                                  default=False,
                                  help_text=_("Tick the box if you want 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']

    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

        # If the exercise has only one image, mark it as main
        if not ExerciseImage.objects.filter(exercise=self.exercise, is_main=False).count():
            self.is_main = True

        #
        # Reset all cached infos
        #
        delete_template_fragment_cache('muscle-overview', self.exercise.language_id)
        delete_template_fragment_cache('exercise-overview', self.exercise.language_id)
        delete_template_fragment_cache('exercise-detail-header',
                                       self.exercise.id,
                                       self.exercise.language_id)
        delete_template_fragment_cache('exercise-detail-muscles',
                                       self.exercise.id,
                                       self.exercise.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)

        delete_template_fragment_cache('muscle-overview', self.exercise.language_id)
        delete_template_fragment_cache('exercise-overview', self.exercise.language_id)
        delete_template_fragment_cache('exercise-detail-header', self.exercise.language_id)
        delete_template_fragment_cache('exercise-detail-muscles', self.exercise.language_id)

        # Make sure there is always a main image
        if not ExerciseImage.objects.filter(exercise=self.exercise, is_main=True).count():
            image = ExerciseImage.objects.filter(exercise=self.exercise).filter(is_main=False)[0]
            image.is_main = True
            image.save()

    def get_owner_object(self):
        '''
        Image has no owner information
        '''
        return False

def delete_exercise_image_on_delete(sender, instance, **kwargs):
    '''
    Delete the image, along with its thumbnails, from the disk
    '''

    thumbnailer = get_thumbnailer(instance.image)
    thumbnailer.delete_thumbnails()
    instance.image.delete(save=False)

post_delete.connect(delete_exercise_image_on_delete, sender=ExerciseImage)

def delete_exercise_image_on_update(sender, instance, **kwargs):
    '''
    Delete the corresponding image from the filesystem when the an ExerciseImage
    object was changed
    '''
    if not instance.pk:
        return False

    try:
        old_file = ExerciseImage.objects.get(pk=instance.pk).image
    except ExerciseImage.DoesNotExist:
        return False

    new_file = instance.image
    if not old_file == new_file:
        thumbnailer = get_thumbnailer(instance.image)
        thumbnailer.delete_thumbnails()
        instance.image.delete(save=False)

pre_save.connect(delete_exercise_image_on_update, sender=ExerciseImage)

# Generate all thumbnails when uploading a new image
saved_file.connect(generate_aliases_global)

class ExerciseComment(models.Model):
    '''
    Model for an exercise comment
    '''
    exercise = models.ForeignKey(Exercise,
                                 verbose_name=_('Exercise'),
                                 editable=False)
    comment = models.CharField(max_length=200,
                               verbose_name=_('Comment'),
                               help_text=_('A comment about how to correctly do this exercise.'))

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.comment

    def get_owner_object(self):
        '''
        Comment has no owner information
        '''
        return False
<commit_msg>Delete the exercise template caches in all available languages<commit_after># -*- coding: utf-8 -*-

# This file is part of wger Workout Manager.
#
# wger Workout Manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# wger Workout Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Workout Manager.  If not, see <http://www.gnu.org/licenses/>.

import logging

from django.db import models
from django.template.defaultfilters import slugify  # django.utils.text.slugify in django 1.5!
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.urlresolvers import reverse
from django.core import mail
from django.core.cache import cache
from django.db.models.signals import pre_save
from django.db.models.signals import post_delete

from easy_thumbnails.files import get_thumbnailer
from easy_thumbnails.signals import saved_file
from easy_thumbnails.signal_handlers import generate_aliases_global

from wger.utils.constants import EMAIL_FROM
from wger.utils.cache import delete_template_fragment_cache
from wger.utils.cache import cache_mapper

logger = logging.getLogger('wger.custom')

class Language(models.Model):
    '''
    Language of an item (exercise, workout, etc.)
    '''

    #e.g. 'de'
    short_name = models.CharField(max_length=2,
                                  verbose_name=_('Language short name'))

    #e.g. 'Deutsch'
    full_name = models.CharField(max_length=30,
                                 verbose_name=_('Language full name'))

    #
    # Django methods
    #
    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return u"{0} ({1})".format(self.full_name, self.short_name)

    def get_absolute_url(self):
        '''
        Returns the canonical URL to view a language
        '''
        return reverse('config:language-view', kwargs={'pk': self.id})

    #
    # Own methods
    #
    def get_owner_object(self):
        '''
        Muscle has no owner information
        '''
        return False

class Muscle(models.Model):
    '''
    Muscle an exercise works out
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('Name'),
                            help_text=_('In latin, e.g. "Pectoralis major"'))

    # Whether to use the front or the back image for background
    is_front = models.BooleanField(default=1)

    # Metaclass to set some other properties
    class Meta:
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Muscle has no owner information
        '''
        return False

class Equipment(models.Model):
    '''
    Equipment used or needed by an exercise
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('Name'))

    class Meta:
        '''
        Set default ordering
        '''
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Equipment has no owner information
        '''
        return False

class ExerciseCategory(models.Model):
    '''
    Model for an exercise category
    '''
    name = models.CharField(max_length=100,
                            verbose_name=_('Name'),)

    # Metaclass to set some other properties
    class Meta:
        verbose_name_plural = _("Exercise Categories")
        ordering = ["name", ]

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.name

    def get_owner_object(self):
        '''
        Category has no owner information
        '''
        return False

class Exercise(models.Model):
    '''
    Model for an exercise
    '''

    EXERCISE_STATUS_PENDING = '1'
    EXERCISE_STATUS_ACCEPTED = '2'
    EXERCISE_STATUS_DECLINED = '3'
    EXERCISE_STATUS_ADMIN = '4'
    EXERCISE_STATUS_SYSTEM = '5'

    EXERCISE_STATUS = (
        (EXERCISE_STATUS_PENDING, _('Pending')),
        (EXERCISE_STATUS_ACCEPTED, _('Accepted')),
        (EXERCISE_STATUS_DECLINED, _('Declined')),
        (EXERCISE_STATUS_ADMIN, _('Submitted by administrator')),
        (EXERCISE_STATUS_SYSTEM, _('System exercise')),
    )

    EXERCISE_STATUS_OK = (EXERCISE_STATUS_ACCEPTED,
                          EXERCISE_STATUS_ADMIN,
                          EXERCISE_STATUS_SYSTEM)

    category = models.ForeignKey(ExerciseCategory,
                                 verbose_name=_('Category'))
    description = models.TextField(max_length=2000,
                                   verbose_name=_('Description'))
    '''Description on how to perform the exercise'''

    name = models.CharField(max_length=200,
                            verbose_name=_('Name'))

    muscles = models.ManyToManyField(Muscle,
                                     verbose_name=_('Primary muscles'),
                                     )
    '''Main muscles trained by the exercise'''

    muscles_secondary = models.ManyToManyField(Muscle,
                                               verbose_name=_('Secondary muscles'),
                                               related_name='secondary_muscles',
                                               null=True,
                                               blank=True
                                               )
    '''Secondary muscles trained by the exercise'''

    equipment = models.ManyToManyField(Equipment,
                                       verbose_name=_('Equipment'),
                                       null=True,
                                       blank=True)
    '''Equipment needed by this exercise'''

    # Non-editable fields
    user = models.ForeignKey(User, verbose_name=_('User'), null=True, blank=True)
    '''The user that submitted the exercise'''

    status = models.CharField(max_length=2,
                              choices=EXERCISE_STATUS,
                              default=EXERCISE_STATUS_PENDING,
                              editable=False)
    '''Status, e.g. accepted or declined'''

    creation_date = models.DateField(_('Date'), auto_now_add=True, null=True, blank=True)
    '''The submission date'''

    language = models.ForeignKey(Language,
                                 verbose_name=_('Language'),
                                 editable=False)
    '''The exercise's language'''

    #
    # Django methods
    #

    # Metaclass to set some other properties
    class Meta:
        ordering = ["name", ]

    def get_absolute_url(self):
        '''
        Returns the canonical URL to view an exercise
        '''
        return reverse('wger.exercises.views.exercises.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-detail-header', self.id, language.id)
            delete_template_fragment_cache('exercise-detail-muscles', self.id, language.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-detail-header', self.id, language.id)
            delete_template_fragment_cache('exercise-detail-muscles', self.id, language.id)

        super(Exercise, self).delete(*args, **kwargs)

    def __unicode__(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
        '''
        has_image = self.exerciseimage_set.exists()
        image = None
        if has_image:
            image = self.exerciseimage_set.filter(is_main=True)[0]
        return image

    def get_owner_object(self):
        '''
        Exercise has no owner information
        '''
        return False

    def send_email(self, request):
        '''
        Sends an email after being sucessfully added to the database (for user
        submitted exercises only)
        '''
        if self.user and self.user.email:
            url = request.build_absolute_uri(self.get_absolute_url())
            subject = _('Exercise was sucessfully added to the general database')
            message = (ugettext("Your exercise '{0}' was successfully added to the general"
                       "database.\n\n"
                       "It is now available on the exercise and muscle overview and can be\n"
                       "added to workouts. You can access it on this address:\n"
                       "{1}\n\n").format(self.name, url) +
                       ugettext("Thank you for contributing and making this site better!\n"
                                "   the wger.de team"))
            mail.send_mail(subject,
                           message,
                           EMAIL_FROM,
                           [self.user.email],
                           fail_silently=True)

def exercise_image_upload_dir(instance, filename):
    '''
    Returns the upload target for exercise images
    '''
    return "exercise-images/{0}/{1}".format(instance.exercise.id, filename)

class ExerciseImage(models.Model):
    '''
    Model for an exercise image
    '''

    IMAGE_LICENSE_CC_BY_SA_3 = '1'
    IMAGE_LICENSE_PUBLIC_DOMAIN = '2'

    IMAGE_LICENSE = (
        (IMAGE_LICENSE_CC_BY_SA_3, 'CC-BY-SA 3.0'),
        (IMAGE_LICENSE_PUBLIC_DOMAIN, _('Public domain')),
    )

    exercise = models.ForeignKey(Exercise,
                                 verbose_name=_('Exercise'),
                                 editable=False)
    '''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'''

    license = models.CharField(verbose_name=_('License'),
                               max_length=2,
                               choices=IMAGE_LICENSE,
                               default=IMAGE_LICENSE_CC_BY_SA_3)
    '''The license the image is released under'''

    license_author = models.CharField(verbose_name=_('Author'),
                                      max_length=50,
                                      blank=True,
                                      null=True,
                                      help_text=_('If you are not the author of the image, enter '
                                                  'the name here. This is needed for some licenses '
                                                  'e.g. the CC-BY-SA.'))
    '''The author of the image if it is not the uploader'''

    is_main = models.BooleanField(verbose_name=_('Is main picture'),
                                  default=False,
                                  help_text=_("Tick the box if you want 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']

    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

        # If the exercise has only one image, mark it as main
        if not ExerciseImage.objects.filter(exercise=self.exercise, is_main=False).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-detail-header',
                                           self.exercise.id,
                                           language.id)
            delete_template_fragment_cache('exercise-detail-muscles',
                                           self.exercise.id,
                                           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-detail-header', self.exercise.id, language.id)
            delete_template_fragment_cache('exercise-detail-muscles', self.exercise.id, language.id)

        # Make sure there is always a main image
        if not ExerciseImage.objects.filter(exercise=self.exercise, is_main=True).count():
            image = ExerciseImage.objects.filter(exercise=self.exercise).filter(is_main=False)[0]
            image.is_main = True
            image.save()

    def get_owner_object(self):
        '''
        Image has no owner information
        '''
        return False

def delete_exercise_image_on_delete(sender, instance, **kwargs):
    '''
    Delete the image, along with its thumbnails, from the disk
    '''

    thumbnailer = get_thumbnailer(instance.image)
    thumbnailer.delete_thumbnails()
    instance.image.delete(save=False)

post_delete.connect(delete_exercise_image_on_delete, sender=ExerciseImage)

def delete_exercise_image_on_update(sender, instance, **kwargs):
    '''
    Delete the corresponding image from the filesystem when the an ExerciseImage
    object was changed
    '''
    if not instance.pk:
        return False

    try:
        old_file = ExerciseImage.objects.get(pk=instance.pk).image
    except ExerciseImage.DoesNotExist:
        return False

    new_file = instance.image
    if not old_file == new_file:
        thumbnailer = get_thumbnailer(instance.image)
        thumbnailer.delete_thumbnails()
        instance.image.delete(save=False)

pre_save.connect(delete_exercise_image_on_update, sender=ExerciseImage)

# Generate all thumbnails when uploading a new image
saved_file.connect(generate_aliases_global)

class ExerciseComment(models.Model):
    '''
    Model for an exercise comment
    '''
    exercise = models.ForeignKey(Exercise,
                                 verbose_name=_('Exercise'),
                                 editable=False)
    comment = models.CharField(max_length=200,
                               verbose_name=_('Comment'),
                               help_text=_('A comment about how to correctly do this exercise.'))

    def __unicode__(self):
        '''
        Return a more human-readable representation
        '''
        return self.comment

    def get_owner_object(self):
        '''
        Comment has no owner information
        '''
        return False
<|endoftext|>


<commit_before>class ContentContext(object):
    """
        A helper class that wraps content related stuff
        for easy access in templates
    """

    def __init__(self, content):
        self.content = content

    def title(self):
        ## deprecated through __getattr__
        return self.content.instance.title

    def url(self):
        return self.content.instance.get_absolute_url()

    def __getattr__(self, attribute):
        return getattr(self.content.instance, attribute)

class ContextWrappable(object):
    contextclass = ContentContext

    def ctx(self, *args, **kwargs):
        """ return a context wrapped version of self """
        return self.contextclass(self, *args, **kwargs)
<commit_msg>expose the spoke's workflow state<commit_after>class ContentContext(object):
    """
        A helper class that wraps content related stuff
        for easy access in templates
    """

    def __init__(self, content):
        self.content = content

    def title(self):
        ## deprecated through __getattr__
        return self.content.instance.title

    def url(self):
        return self.content.instance.get_absolute_url()

    def state(self):
        return self.content.state()

    def __getattr__(self, attribute):
        return getattr(self.content.instance, attribute)

class ContextWrappable(object):
    contextclass = ContentContext

    def ctx(self, *args, **kwargs):
        """ return a context wrapped version of self """
        return self.contextclass(self, *args, **kwargs)
<|endoftext|>


<commit_before>from django.contrib import admin
from django.conf.urls.defaults import patterns, include

from django.conf import settings
from django.views.defaults import page_not_found

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from wheelcms_axle.urls import handler500, handler404

admin.autodiscover()

basepatterns = staticfiles_urlpatterns()

## or import static_urls; urlpatterns = static_urls.urlpatterns

if not settings.ALLOW_SIGNUP:
    basepatterns += patterns('',
        ('^accounts/signup', page_not_found),
    )

basepatterns += patterns('',
    (r'^admin/', include(admin.site.urls)),
)
wheelpatterns = patterns('',
    (r'^favicon.ico$', 'django.views.generic.simple.redirect_to',
                        {'url': '/static/images/favicon.ico'}),
    (r'^tinymce/', include('tinymce.urls')),
    (r'^accounts/', include('userena.urls')),
    (r'^search/', include('haystack.urls')),
    (r'', include('wheelcms_axle.urls')),
)

if settings.DEBUG:
    basepatterns += patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve',
         {'document_root': settings.MEDIA_ROOT}),
    )

urlpatterns = basepatterns + wheelpatterns
<commit_msg>simple.redirect_to has been deprecated<commit_after>from django.contrib import admin
from django.conf.urls.defaults import patterns, include

from django.conf import settings
from django.views.defaults import page_not_found

from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from wheelcms_axle.urls import handler500, handler404

from django.views.generic import RedirectView

admin.autodiscover()

basepatterns = staticfiles_urlpatterns()

## or import static_urls; urlpatterns = static_urls.urlpatterns

if not settings.ALLOW_SIGNUP:
    basepatterns += patterns('',
        ('^accounts/signup', page_not_found),
    )

basepatterns += patterns('',
    (r'^admin/', include(admin.site.urls)),
)
wheelpatterns = patterns('',
    (r'^favicon.ico$', RedirectView.as_view(url='/static/images/favicon.ico')),
    (r'^tinymce/', include('tinymce.urls')),
    (r'^accounts/', include('userena.urls')),
    (r'^search/', include('haystack.urls')),
    (r'', include('wheelcms_axle.urls')),
)

if settings.DEBUG:
    basepatterns += patterns('',
        (r'^media/(?P<path>.*)$', 'django.views.static.serve',
         {'document_root': settings.MEDIA_ROOT}),
    )

urlpatterns = basepatterns + wheelpatterns
<|endoftext|>


<commit_before>__version__ = '0.2.0'
<commit_msg>Incremented version to 0.2.1<commit_after>__version__ = '0.2.1'
<|endoftext|>


<commit_before>#   Copyright (c) 2006-2007 Open Source Applications Foundation
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import windmill

from httplib import HTTPConnection
from urlparse import urlparse
import copy
import logging
import urllib
logger = logging.getLogger(__name__)
from forwardmanager import ForwardManager

# Note that hoppish conntains proxy-connection, which is pre-HTTP-1.1 and
# is somewhat nebulous
_hoppish = {
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1, 'proxy-connection':1,
    'p3p':1 #Not actually a hop-by-hop header, just really annoying
    }

# Cache stopping headers
cache_headers = {'Pragma':'no-cache', 'Cache-Control': 'post-check=0, pre-check=0',
                 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
                 'Expires': '-1'}

cache_removal = [k.lower() for k in cache_headers.keys()]
cache_additions = cache_headers.items()

def is_hop_by_hop(header):
    """check if the given header is hop_by_hop"""
    return _hoppish.has_key(header.lower())

class IterativeResponse(object):
    def __init__(self, response_instance):
        self.response_instance = response_instance

    def __iter__(self):
        if self.response_instance.chunked:
            yield self.response_instance.read(1024)
            while self.response_instance.chunk_left is not None:
                if self.response_instance.chunk_left < 1024:
                    yield self.response_instance.read()
                    self.response_instance.chunk_left = None
                else:
                    yield self.response_instance.read(1024)
        else:
            yield self.response_instance.read()

def get_wsgi_response(response):
    if type(response) is str:
        return [response]
    if response.length > 1000:
        return IterativeResponse(response)
    else:
        return [response.read()]

def conditions_pass(e):
    for c in windmill.server.forwarding_conditions:
        if c(e) is False:
            return False
    return True

def proxy_post_redirect_form(environ, action):
    body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    parameters = body.split('&')
    inputs = []
    for parameter in parameters:
        parts = parameter.split('=', 1)
        if len(parts) == 1:
            continue
        parts = tuple(urllib.unquote(part) for part in parts)
        inputs.append('<input type="hidden" name="%s" value="%s" />' % parts)
    form = """<title>There is no spoon.</title>

  <form id="redirect" action="%s" method="POST">%s</form>
""" % (action, '\n'.join(inputs))
    return form

class WindmillProxyApplication(object):
    """Application to handle requests that need to be proxied"""

    def __init__(self):
        self.fmgr = None
        proxyInstances.append(self)

    ConnectionClass = HTTPConnection

    def handler(self, environ, start_response):
        """Proxy for requests to the actual http server"""
        url = urlparse(environ['reconstructed_url'])

        # Once FORWARDING_TEST_URL is set we should check for cross-domain
        # forward but we must disable for localhost as redirects to localhost
        # will cause the browser to error.
        if windmill.settings['FORWARDING_TEST_URL'] is not None and (
#           not url.netloc.startswith('localhost') ) and (
#           not url.netloc.startswith('127.0.0.1') ) and (
           conditions_pass(environ) ):
            test_url = windmill.settings['FORWARDING_TEST_URL']
            if self.fmgr is None:
                # Be lazy at creating the forward manager to give
                # FORWARDING_TEST_URL a chance to be set
                self.fmgr = ForwardManager(test_url)
            # Do our domain change magic
            url = urlparse(environ['reconstructed_url'])
            test_netloc = urlparse(test_url).netloc
            referer = environ.get('HTTP_REFERER', None)

            if (self.fmgr.is_static_forwarded(url)):
                environ = self.fmgr.forward(url, environ)
                url = self.fmgr.forward_map(url)

            elif ( url.netloc != test_netloc ):
                # if the url's network address is not the test URL that has
                # been set we need to return a forward
                environ = self.fmgr.forward(url, environ)
                redirect_url = self.fmgr.forward_map(url).geturl()
                if environ['REQUEST_METHOD'] == 'POST':
                    response = proxy_post_redirect_form(environ, redirect_url)
                    length = str(len(response))
                    start_response("200 Ok", [('Content-Type', 'text/html',),
                                              ('Content-Length', length,),
                                             ]+cache_additions)
                else:
                    response = 'Windmill is forwarding you to a new url at the proper test domain'
                    length = str(len(response))
                    start_response("302 Found", [('Content-Type', 'text/plain',),
                                                 ('Content-Length', length,),
                                                 ('Location', redirect_url),
                                                 ]+cache_additions)
                logger.debug('Domain change, forwarded to ' + redirect_url)
                return [response]

            elif (self.fmgr.is_forward_mapped(url)):
                orig_url = self.fmgr.forward_unmap(url)
                environ = self.fmgr.change_environ_domain(url, orig_url,
                                                          environ)
                url = orig_url

        def make_remote_connection(url, environ):
            # Create connection object
            try:
                connection = self.get_connection(url)
                # Build path
                path = url.geturl().replace(url.scheme+'://'+url.netloc, '')
            except Exception, e:
                logger.exception('Could not Connect')
                return [("501 Gateway error", [('Content-Type', 'text/html')],),
                        '
Could not connect:

%s
' % (str(e),)]

            # Read in request body if it exists
            body = None
            if environ.has_key('body'):
                body = environ['body']
            elif environ.get('CONTENT_LENGTH'):
                length = int(environ['CONTENT_LENGTH'])
                body = environ['wsgi.input'].read(length)
                environ['body'] = body

            # Build headers
            headers = {}
            logger.debug('Environ ; %s' % str(environ))
            for key in environ.keys():
                # Keys that start with HTTP_ are all headers
                if key.startswith('HTTP_'):
                    # This is a hacky way of getting the header names right
                    value = environ[key]
                    key = key.replace('HTTP_', '', 1).swapcase().replace('_', '-')
                    if is_hop_by_hop(key) is False:
                        headers[key] = value
                    if key.lower() == 'location':
                        # There should never be a legitimate redirect
                        # to /windmill-serv from a remote site
                        if '/windmill-serv' in value:
                            value = value.split('/windmill-serv')[0]

            # Handler headers that aren't HTTP_ in environ
            if environ.get('CONTENT_TYPE'):
                headers['content-type'] = environ['CONTENT_TYPE']

            # Add our host if one isn't defined
            if not headers.has_key('host'):
                headers['host'] = environ['SERVER_NAME']

            # Make the remote request
            try:

                logger.debug('%s %s %s' % (environ['REQUEST_METHOD'], path,
                                           str(headers)))
                connection.request(environ['REQUEST_METHOD'], path, body=body,
                                   headers=headers)
                connection.url = url
                return connection
            except Exception, e:
                # We need extra exception handling in the case the server
                # fails in mid connection, it's an edge case but I've seen it
                return [("501 Gateway error", [('Content-Type', 'text/html')],),
                    '
Could not make request:

%s
' % (str(e),)]

        def retry_known_hosts(url, environ):
            # retry the given request against all the hosts the current session
            # has run against
            if self.fmgr is None:
                return
            for host in self.fmgr.known_hosts():
                orig_url = self.fmgr.forward_to(url, host)
                connection = make_remote_connection(orig_url,
                                 self.fmgr.change_environ_domain(
                                        self.fmgr.forward_map(orig_url),
                                        orig_url, environ))
                if isinstance(connection, HTTPConnection):
                    new_response = connection.getresponse()
                    if new_response.status > 199 and new_response.status < 399:
                        logger.info('Retry success, ' + url.geturl() + ' to ' +
                                    host.geturl())
                        new_response.url = connection.url
                        return new_response

        connection = make_remote_connection(url, environ)
        # This following code is ugly.  It should be refactored in to some
        # elegant way to decide when to retry, and which URLs to retry.
        # Maybe hand that responsability to the ForwardManager?
        if isinstance(connection, HTTPConnection):
            response = connection.getresponse()
            response.url = connection.url
        if not isinstance(connection, HTTPConnection) or \
            response.status in [404, 500]:
            # if it's not an HTTPConnection object then the request failed
            if (not self.fmgr.is_forward_mapped(url) and
               referer is not None and
               self.fmgr.is_forward_mapped(urlparse(referer))):
                # This handles the case that the referer is a url we've already
                # done a cross-domain request for
                new_environ = self.fmgr.forward(url, environ)
                new_url = self.fmgr.forward_map(url)
                connection = make_remote_connection(url, environ)
                if isinstance(connection, HTTPConnection):
                    response = connection.getresponse()
                    response.url = connection.url
                    if response.status not in [404, 500]:
                        url = new_url
                        environ = new_environ
            if not isinstance(connection, HTTPConnection) or \
               response.status in [404, 500]:
                # now yes, we should retry
                new_response = retry_known_hosts(url, environ)
                if new_response is not None:
                    response = new_response
                elif not isinstance(connection, HTTPConnection):
                    status = connection[0][0]
                    headers = connection[0][1]
                    body = connection[1]
                    for header in copy.copy(headers):
                        if header[0].lower() in cache_removal:
                            headers.remove(header)
                    start_response(status, headers+cache_additions)
                    return get_wsgi_response(body)

        # Remove hop by hop headers
        headers = self.parse_headers(response)
        if response.status == 404:
            logger.info('Could not fullfill proxy request to ' + url.geturl())

        for header in copy.copy(headers):
            if header[0].lower() in cache_removal:
                headers.remove(header)

        start_response(response.status.__str__()+' '+response.reason,
                       headers+cache_additions)
        return get_wsgi_response(response)

    def parse_headers(self, response):
        headers = [(x.lower(), y) for x, y in [z.split(':', 1) for z in
                             str(response.msg).splitlines() if ':' in z]]
        if self.fmgr is not None:
            self.fmgr.parse_headers(headers, response.url.netloc)
        for header in headers:
            if is_hop_by_hop(header[0]):
                headers.remove(header)
            elif header[0] == 'location':
                # There should never be a legitimate redirect to /windmill-serv
                # from a remote site
                if '/windmill-serv' in header[1]:
                    i = headers.index(header)
                    location = header[1]
                    headers.remove(header)
                    headers.insert(i, ('location',
                                   location.split('/windmill-serv')[0],))
        return headers

    def get_connection(self, url):
        """ Factory method for connections """
        connection = self.ConnectionClass(url.netloc)
        return connection

    def clearForwardingRegistry(self):
        if self.fmgr is not None:
            self.fmgr.clear()

    def __call__(self, environ, start_response):
        return self.handler(environ, start_response)

proxyInstances = []
def clearForwardingRegistry():
    for p in proxyInstances:
        p.clearForwardingRegistry()
<commit_msg>Fixing encode/decode bug in proxy.py.<commit_after>#   Copyright (c) 2006-2007 Open Source Applications Foundation
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import windmill

from httplib import HTTPConnection
from urlparse import urlparse
import copy
import logging
import urllib
logger = logging.getLogger(__name__)
from forwardmanager import ForwardManager

# Note that hoppish conntains proxy-connection, which is pre-HTTP-1.1 and
# is somewhat nebulous
_hoppish = {
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1, 'proxy-connection':1,
    'p3p':1 #Not actually a hop-by-hop header, just really annoying
    }

# Cache stopping headers
cache_headers = {'Pragma':'no-cache', 'Cache-Control': 'post-check=0, pre-check=0',
                 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
                 'Expires': '-1'}

cache_removal = [k.lower() for k in cache_headers.keys()]
cache_additions = cache_headers.items()

def is_hop_by_hop(header):
    """check if the given header is hop_by_hop"""
    return _hoppish.has_key(header.lower())

class IterativeResponse(object):
    def __init__(self, response_instance):
        self.response_instance = response_instance

    def __iter__(self):
        if self.response_instance.chunked:
            yield self.response_instance.read(1024)
            while self.response_instance.chunk_left is not None:
                if self.response_instance.chunk_left < 1024:
                    yield self.response_instance.read()
                    self.response_instance.chunk_left = None
                else:
                    yield self.response_instance.read(1024)
        else:
            yield self.response_instance.read()

def get_wsgi_response(response):
    if type(response) is str:
        return [response]
    if response.length > 1000:
        return IterativeResponse(response)
    else:
        return [response.read()]

def conditions_pass(e):
    for c in windmill.server.forwarding_conditions:
        if c(e) is False:
            return False
    return True

def proxy_post_redirect_form(environ, action):
    body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    parameters = body.split('&')
    inputs = []
    for parameter in parameters:
        parts = parameter.split('=', 1)
        if len(parts) == 1:
            continue
        parts = tuple(unicode(urllib.unquote(part), 'utf-8') for part in parts)
        inputs.append('<input type="hidden" name="%s" value="%s" />' % parts)
    form = """<title>There is no spoon.</title>

  <form id="redirect" action="%s" method="POST">%s</form>
""" % (action, '\n'.join(inputs))
    return form.encode('utf-8')

class WindmillProxyApplication(object):
    """Application to handle requests that need to be proxied"""

    def __init__(self):
        self.fmgr = None
        proxyInstances.append(self)

    ConnectionClass = HTTPConnection

    def handler(self, environ, start_response):
        """Proxy for requests to the actual http server"""
        url = urlparse(environ['reconstructed_url'])

        # Once FORWARDING_TEST_URL is set we should check for cross-domain
        # forward but we must disable for localhost as redirects to localhost
        # will cause the browser to error.
        if windmill.settings['FORWARDING_TEST_URL'] is not None and (
#           not url.netloc.startswith('localhost') ) and (
#           not url.netloc.startswith('127.0.0.1') ) and (
           conditions_pass(environ) ):
            test_url = windmill.settings['FORWARDING_TEST_URL']
            if self.fmgr is None:
                # Be lazy at creating the forward manager to give
                # FORWARDING_TEST_URL a chance to be set
                self.fmgr = ForwardManager(test_url)
            # Do our domain change magic
            url = urlparse(environ['reconstructed_url'])
            test_netloc = urlparse(test_url).netloc
            referer = environ.get('HTTP_REFERER', None)

            if (self.fmgr.is_static_forwarded(url)):
                environ = self.fmgr.forward(url, environ)
                url = self.fmgr.forward_map(url)

            elif ( url.netloc != test_netloc ):
                # if the url's network address is not the test URL that has
                # been set we need to return a forward
                environ = self.fmgr.forward(url, environ)
                redirect_url = self.fmgr.forward_map(url).geturl()
                if environ['REQUEST_METHOD'] == 'POST':
                    response = proxy_post_redirect_form(environ, redirect_url)
                    length = str(len(response))
                    start_response("200 Ok", [('Content-Type', 'text/html',),
                                              ('Content-Length', length,),
                                             ]+cache_additions)
                else:
                    response = 'Windmill is forwarding you to a new url at the proper test domain'
                    length = str(len(response))
                    start_response("302 Found", [('Content-Type', 'text/plain',),
                                                 ('Content-Length', length,),
                                                 ('Location', redirect_url),
                                                 ]+cache_additions)
                logger.debug('Domain change, forwarded to ' + redirect_url)
                return [response]

            elif (self.fmgr.is_forward_mapped(url)):
                orig_url = self.fmgr.forward_unmap(url)
                environ = self.fmgr.change_environ_domain(url, orig_url,
                                                          environ)
                url = orig_url

        def make_remote_connection(url, environ):
            # Create connection object
            try:
                connection = self.get_connection(url)
                # Build path
                path = url.geturl().replace(url.scheme+'://'+url.netloc, '')
            except Exception, e:
                logger.exception('Could not Connect')
                return [("501 Gateway error", [('Content-Type', 'text/html')],),
                        '
Could not connect:

%s
' % (str(e),)]

            # Read in request body if it exists
            body = None
            if environ.has_key('body'):
                body = environ['body']
            elif environ.get('CONTENT_LENGTH'):
                length = int(environ['CONTENT_LENGTH'])
                body = environ['wsgi.input'].read(length)
                environ['body'] = body

            # Build headers
            headers = {}
            logger.debug('Environ ; %s' % str(environ))
            for key in environ.keys():
                # Keys that start with HTTP_ are all headers
                if key.startswith('HTTP_'):
                    # This is a hacky way of getting the header names right
                    value = environ[key]
                    key = key.replace('HTTP_', '', 1).swapcase().replace('_', '-')
                    if is_hop_by_hop(key) is False:
                        headers[key] = value
                    if key.lower() == 'location':
                        # There should never be a legitimate redirect
                        # to /windmill-serv from a remote site
                        if '/windmill-serv' in value:
                            value = value.split('/windmill-serv')[0]

            # Handler headers that aren't HTTP_ in environ
            if environ.get('CONTENT_TYPE'):
                headers['content-type'] = environ['CONTENT_TYPE']

            # Add our host if one isn't defined
            if not headers.has_key('host'):
                headers['host'] = environ['SERVER_NAME']

            # Make the remote request
            try:

                logger.debug('%s %s %s' % (environ['REQUEST_METHOD'], path,
                                           str(headers)))
                connection.request(environ['REQUEST_METHOD'], path, body=body,
                                   headers=headers)
                connection.url = url
                return connection
            except Exception, e:
                # We need extra exception handling in the case the server
                # fails in mid connection, it's an edge case but I've seen it
                return [("501 Gateway error", [('Content-Type', 'text/html')],),
                    '
Could not make request:

%s
' % (str(e),)]

        def retry_known_hosts(url, environ):
            # retry the given request against all the hosts the current session
            # has run against
            if self.fmgr is None:
                return
            for host in self.fmgr.known_hosts():
                orig_url = self.fmgr.forward_to(url, host)
                connection = make_remote_connection(orig_url,
                                 self.fmgr.change_environ_domain(
                                        self.fmgr.forward_map(orig_url),
                                        orig_url, environ))
                if isinstance(connection, HTTPConnection):
                    new_response = connection.getresponse()
                    if new_response.status > 199 and new_response.status < 399:
                        logger.info('Retry success, ' + url.geturl() + ' to ' +
                                    host.geturl())
                        new_response.url = connection.url
                        return new_response

        connection = make_remote_connection(url, environ)
        # This following code is ugly.  It should be refactored in to some
        # elegant way to decide when to retry, and which URLs to retry.
        # Maybe hand that responsability to the ForwardManager?
        if isinstance(connection, HTTPConnection):
            response = connection.getresponse()
            response.url = connection.url
        if not isinstance(connection, HTTPConnection) or \
            response.status in [404, 500]:
            # if it's not an HTTPConnection object then the request failed
            if (not self.fmgr.is_forward_mapped(url) and
               referer is not None and
               self.fmgr.is_forward_mapped(urlparse(referer))):
                # This handles the case that the referer is a url we've already
                # done a cross-domain request for
                new_environ = self.fmgr.forward(url, environ)
                new_url = self.fmgr.forward_map(url)
                connection = make_remote_connection(url, environ)
                if isinstance(connection, HTTPConnection):
                    response = connection.getresponse()
                    response.url = connection.url
                    if response.status not in [404, 500]:
                        url = new_url
                        environ = new_environ
            if not isinstance(connection, HTTPConnection) or \
               response.status in [404, 500]:
                # now yes, we should retry
                new_response = retry_known_hosts(url, environ)
                if new_response is not None:
                    response = new_response
                elif not isinstance(connection, HTTPConnection):
                    status = connection[0][0]
                    headers = connection[0][1]
                    body = connection[1]
                    for header in copy.copy(headers):
                        if header[0].lower() in cache_removal:
                            headers.remove(header)
                    start_response(status, headers+cache_additions)
                    return get_wsgi_response(body)

        # Remove hop by hop headers
        headers = self.parse_headers(response)
        if response.status == 404:
            logger.info('Could not fullfill proxy request to ' + url.geturl())

        for header in copy.copy(headers):
            if header[0].lower() in cache_removal:
                headers.remove(header)

        start_response(response.status.__str__()+' '+response.reason,
                       headers+cache_additions)
        return get_wsgi_response(response)

    def parse_headers(self, response):
        headers = [(x.lower(), y) for x, y in [z.split(':', 1) for z in
                             str(response.msg).splitlines() if ':' in z]]
        if self.fmgr is not None:
            self.fmgr.parse_headers(headers, response.url.netloc)
        for header in headers:
            if is_hop_by_hop(header[0]):
                headers.remove(header)
            elif header[0] == 'location':
                # There should never be a legitimate redirect to /windmill-serv
                # from a remote site
                if '/windmill-serv' in header[1]:
                    i = headers.index(header)
                    location = header[1]
                    headers.remove(header)
                    headers.insert(i, ('location',
                                   location.split('/windmill-serv')[0],))
        return headers

    def get_connection(self, url):
        """ Factory method for connections """
        connection = self.ConnectionClass(url.netloc)
        return connection

    def clearForwardingRegistry(self):
        if self.fmgr is not None:
            self.fmgr.clear()

    def __call__(self, environ, start_response):
        return self.handler(environ, start_response)

proxyInstances = []
def clearForwardingRegistry():
    for p in proxyInstances:
        p.clearForwardingRegistry()
<|endoftext|>


<commit_before>#   Copyright (c) 2006-2007 Open Source Applications Foundation
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import windmill

from httplib import HTTPConnection
from urlparse import urlparse
import copy
import logging

logger = logging.getLogger(__name__)

# Note that hoppish conntains proxy-connection, which is pre-HTTP-1.1 and is somewhat nebulous
_hoppish = {
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1, 'proxy-connection':1,
    'p3p':1 #Not actually a hop-by-hop header, just really annoying
    }

def is_hop_by_hop(header):
    """check if the given header is hop_by_hop"""
    return _hoppish.has_key(header.lower())

initial_forwarding_registry = {}
forwarding_registry = {}
exclude_from_retry = []

class IterativeResponse(object):
    def __init__(self, response_instance):
        self.response_instance = response_instance

    def __iter__(self):
        if self.response_instance.chunked:
            yield self.response_instance.read(1024)
            while self.response_instance.chunk_left is not None:
                if self.response_instance.chunk_left < 1024:
                    yield self.response_instance.read()
                    self.response_instance.chunk_left = None
                else:
                    yield self.response_instance.read(1024)
        else:
            yield self.response_instance.read()

def get_wsgi_response(response):
    if response.length > 1000:
        return IterativeResponse(response)
    else:
        return [response.read()]

def conditions_pass(e):
    for c in windmill.server.forwarding_conditions:
        if c(e) is False:
            return False
    return True

class WindmillProxyApplication(object):
    """Application to handle requests that need to be proxied"""

    ConnectionClass = HTTPConnection

    def handler(self, environ, start_response):
        """Proxy for requests to the actual http server"""
        url = urlparse(environ['reconstructed_url'])

        def change_environ_domain(original_netloc, new_netloc, environ):
            """Swap out the domain for a given request environ"""
            new_environ = {}
            for key, value in environ.items():
                if ( type(value) is str ) and ( value.find(original_netloc) is not -1 ):
                    new_environ[key] = value.replace(original_netloc, new_netloc)
                else:
                    new_environ[key] = value
            return new_environ

        # Once FORWARDING_TEST_URL is set we should check for cross-domain forward
        # but we must disable for localhost as redirects to localhost will cause the browser
        # to error.
        if windmill.settings['FORWARDING_TEST_URL'] is not None and (
           not url.netloc.startswith('localhost') ) and (
           not url.netloc.startswith('127.0.0.1') ) and (
           conditions_pass(environ) ):
            # Do our domain change magic
            url = urlparse(environ['reconstructed_url'])

            test_netloc = urlparse(windmill.settings['FORWARDING_TEST_URL']).netloc
            referer = environ.get('HTTP_REFERER', None)

            if ( url.netloc != test_netloc ):
                # if the url's network address is not the test URL that has been set we need to return
                # a forward
                initial_forwarding_registry[url.geturl().replace(url.netloc, test_netloc, 1)] = url.netloc
                response = 'Windmill is forwarding you to a new url at the proper test domain'
                start_response("302 Found", [('Content-Type', 'text/plain',),
                                             ('Content-Length', str(len(response)),),
                                             ('Location', url.geturl().replace(url.netloc, test_netloc,),
                                             )])
                logger.debug('New domain request, forwarded to '+url.geturl().replace(url.netloc, test_netloc))
                return [response]

            elif ( url.geturl() in initial_forwarding_registry.keys() ):
                # This handles the first case where a forward is returned to the browser
                host_netloc = initial_forwarding_registry.get(url.geturl())
                forwarding_registry[url.geturl()] = host_netloc
                environ = change_environ_domain(url.netloc, host_netloc, environ)
                url = urlparse(url.geturl().replace(url.netloc, host_netloc))

            elif (referer is not None) and (referer in forwarding_registry.keys()):
                # This handles the case that the referer is a url we've already
                # done a cross-domain request for
                host_netloc = forwarding_registry[referer]
                forwarding_registry[url.geturl()] = host_netloc
                environ = change_environ_domain(url.netloc, host_netloc, environ)
                url = urlparse(url.geturl().replace(url.netloc, host_netloc))

        def make_remote_connection(url, environ):
            # Create connection object
            try:
                connection = self.ConnectionClass(url.netloc)
                # Build path
                path = url.geturl().replace(url.scheme+'://'+url.netloc, '')
            except Exception, e:
                logger.exception('Could not Connect')
                return [("501 Gateway error", [('Content-Type', 'text/html')],), '
Could not connect
']

            # Read in request body if it exists
            body = None
            if environ.get('CONTENT_LENGTH'):
                length = int(environ['CONTENT_LENGTH'])
                body = environ['wsgi.input'].read(length)

            # Build headers
            headers = {}
            logger.debug('Environ ; %s' % str(environ))
            for key in environ.keys():
                # Keys that start with HTTP_ are all headers
                if key.startswith('HTTP_'):
                    # This is a hacky way of getting the header names right
                    value = environ[key]
                    key = key.replace('HTTP_', '', 1).swapcase().replace('_', '-')
                    if is_hop_by_hop(key) is False:
                        headers[key] = value
                    if key.lower() == 'location':
                        # There should never be a legitimate redirect to /windmill-serv from a remote site
                        if '/windmill-serv' in value:
                            value = value.split('/windmill-serv')[0]

            # Handler headers that aren't HTTP_ in environ
            if environ.get('CONTENT_TYPE'):
                headers['content-type'] = environ['CONTENT_TYPE']

            # Add our host if one isn't defined
            if not headers.has_key('host'):
                headers['host'] = environ['SERVER_NAME']

            # Make the remote request
            try:
                logger.debug('%s %s %s' % (environ['REQUEST_METHOD'], path, str(headers)))
                connection.request(environ['REQUEST_METHOD'], path, body=body, headers=headers)
                return connection
            except:
                # We need extra exception handling in the case the server fails in mid connection, it's an edge case but I've seen it
                return [("501 Gateway error", [('Content-Type', 'text/html')],), '
Could not connect
']

        def retry_known_hosts(url, environ):
            # retry the given request against all the hosts the current session has run against
            nhosts = copy.copy(initial_forwarding_registry.values())
            nhosts.reverse()
            hosts = [];
            # Can't use set() here because it needs to keep it's original ordering.
            for h in nhosts:
                if h not in hosts and h not in exclude_from_retry: hosts.append(h)
            current_host = url.netloc
            for host in hosts:
                connection = make_remote_connection(urlparse(url.geturl().replace(current_host, host)),
                                                    change_environ_domain(current_host, host, environ))
                if isinstance(connection, HTTPConnection):
                    new_response = connection.getresponse()
                    if new_response.status > 199 and new_response.status < 399:
                        logger.info('Retry success, '+url.geturl()+' to '+host)
                        return new_response
            return None

        connection = make_remote_connection(url, environ)
        if not isinstance(connection, HTTPConnection):
            # if it's not an HTTPConnection object then the request failed so we should retry
            new_response = retry_known_hosts(url, environ)
            if new_response is not None:
                response = new_response
            else:
                start_response(*connection.pop(0))
                return [get_wsgi_response(connection.pop(0))]
        else:
            response = connection.getresponse()

        if response.status == 404:
            # If the response is 404 we should retry against the known hosts
            # this is usually the case when the referrer is ommitted by the browser
            # for random reasons
            new_response = retry_known_hosts(url, environ)
            if new_response is not None:
                response = new_response

        # Remove hop by hop headers
        headers = self.parse_headers(response)
        if response.status == 404:
            logger.info('Could not fullfill proxy request to '+url.geturl()+'. '+str(set(initial_forwarding_registry.values())))
        start_response(response.status.__str__()+' '+response.reason, headers)
        return get_wsgi_response(response)

    def parse_headers(self, response):
        headers = [ (x.lower(), y,) for x, y in [ z.split(':', 1) for z in str(response.msg).splitlines() if ':' in z]]
        for header in headers:
            if is_hop_by_hop(header[0]):
                headers.remove(header)
            elif header[0] == 'location':
                # There should never be a legitimate redirect to /windmill-serv from a remote site
                if '/windmill-serv' in header[1]:
                    i = headers.index(header)
                    location = header[1]
                    headers.remove(header)
                    headers.insert(i, ('location',location.split('/windmill-serv')[0],))
        return headers

    def __call__(self, environ, start_response):
        return self.handler(environ, start_response)<commit_msg>Fixing up the body case.<commit_after>#   Copyright (c) 2006-2007 Open Source Applications Foundation
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import windmill

from httplib import HTTPConnection
from urlparse import urlparse
import copy
import logging

logger = logging.getLogger(__name__)

# Note that hoppish conntains proxy-connection, which is pre-HTTP-1.1 and is somewhat nebulous
_hoppish = {
    'connection':1, 'keep-alive':1, 'proxy-authenticate':1,
    'proxy-authorization':1, 'te':1, 'trailers':1, 'transfer-encoding':1,
    'upgrade':1, 'proxy-connection':1,
    'p3p':1 #Not actually a hop-by-hop header, just really annoying
    }

def is_hop_by_hop(header):
    """check if the given header is hop_by_hop"""
    return _hoppish.has_key(header.lower())

initial_forwarding_registry = {}
forwarding_registry = {}
exclude_from_retry = []

class IterativeResponse(object):
    def __init__(self, response_instance):
        self.response_instance = response_instance

    def __iter__(self):
        if self.response_instance.chunked:
            yield self.response_instance.read(1024)
            while self.response_instance.chunk_left is not None:
                if self.response_instance.chunk_left < 1024:
                    yield self.response_instance.read()
                    self.response_instance.chunk_left = None
                else:
                    yield self.response_instance.read(1024)
        else:
            yield self.response_instance.read()

def get_wsgi_response(response):
    if type(response) is str:
        return [response]
    if response.length > 1000:
        return IterativeResponse(response)
    else:
        return [response.read()]

def conditions_pass(e):
    for c in windmill.server.forwarding_conditions:
        if c(e) is False:
            return False
    return True

class WindmillProxyApplication(object):
    """Application to handle requests that need to be proxied"""

    ConnectionClass = HTTPConnection

    def handler(self, environ, start_response):
        """Proxy for requests to the actual http server"""
        url = urlparse(environ['reconstructed_url'])

        def change_environ_domain(original_netloc, new_netloc, environ):
            """Swap out the domain for a given request environ"""
            new_environ = {}
            for key, value in environ.items():
                if ( type(value) is str ) and ( value.find(original_netloc) is not -1 ):
                    new_environ[key] = value.replace(original_netloc, new_netloc)
                else:
                    new_environ[key] = value
            return new_environ

        # Once FORWARDING_TEST_URL is set we should check for cross-domain forward
        # but we must disable for localhost as redirects to localhost will cause the browser
        # to error.
        if windmill.settings['FORWARDING_TEST_URL'] is not None and (
           not url.netloc.startswith('localhost') ) and (
           not url.netloc.startswith('127.0.0.1') ) and (
           conditions_pass(environ) ):
            # Do our domain change magic
            url = urlparse(environ['reconstructed_url'])

            test_netloc = urlparse(windmill.settings['FORWARDING_TEST_URL']).netloc
            referer = environ.get('HTTP_REFERER', None)

            if ( url.netloc != test_netloc ):
                # if the url's network address is not the test URL that has been set we need to return
                # a forward
                initial_forwarding_registry[url.geturl().replace(url.netloc, test_netloc, 1)] = url.netloc
                response = 'Windmill is forwarding you to a new url at the proper test domain'
                start_response("302 Found", [('Content-Type', 'text/plain',),
                                             ('Content-Length', str(len(response)),),
                                             ('Location', url.geturl().replace(url.netloc, test_netloc,),
                                             )])
                logger.debug('New domain request, forwarded to '+url.geturl().replace(url.netloc, test_netloc))
                return [response]

            elif ( url.geturl() in initial_forwarding_registry.keys() ):
                # This handles the first case where a forward is returned to the browser
                host_netloc = initial_forwarding_registry.get(url.geturl())
                forwarding_registry[url.geturl()] = host_netloc
                environ = change_environ_domain(url.netloc, host_netloc, environ)
                url = urlparse(url.geturl().replace(url.netloc, host_netloc))

            elif (referer is not None) and (referer in forwarding_registry.keys()):
                # This handles the case that the referer is a url we've already
                # done a cross-domain request for
                host_netloc = forwarding_registry[referer]
                forwarding_registry[url.geturl()] = host_netloc
                environ = change_environ_domain(url.netloc, host_netloc, environ)
                url = urlparse(url.geturl().replace(url.netloc, host_netloc))

        def make_remote_connection(url, environ):
            # Create connection object
            try:
                connection = self.ConnectionClass(url.netloc)
                # Build path
                path = url.geturl().replace(url.scheme+'://'+url.netloc, '')
            except Exception, e:
                logger.exception('Could not Connect')
                return [("501 Gateway error", [('Content-Type', 'text/html')],), '
Could not connect
']

            # Read in request body if it exists
            body = None
            if environ.get('CONTENT_LENGTH'):
                length = int(environ['CONTENT_LENGTH'])
                body = environ['wsgi.input'].read(length)

            # Build headers
            headers = {}
            logger.debug('Environ ; %s' % str(environ))
            for key in environ.keys():
                # Keys that start with HTTP_ are all headers
                if key.startswith('HTTP_'):
                    # This is a hacky way of getting the header names right
                    value = environ[key]
                    key = key.replace('HTTP_', '', 1).swapcase().replace('_', '-')
                    if is_hop_by_hop(key) is False:
                        headers[key] = value
                    if key.lower() == 'location':
                        # There should never be a legitimate redirect to /windmill-serv from a remote site
                        if '/windmill-serv' in value:
                            value = value.split('/windmill-serv')[0]

            # Handler headers that aren't HTTP_ in environ
            if environ.get('CONTENT_TYPE'):
                headers['content-type'] = environ['CONTENT_TYPE']

            # Add our host if one isn't defined
            if not headers.has_key('host'):
                headers['host'] = environ['SERVER_NAME']

            # Make the remote request
            try:
                logger.debug('%s %s %s' % (environ['REQUEST_METHOD'], path, str(headers)))
                connection.request(environ['REQUEST_METHOD'], path, body=body, headers=headers)
                return connection
            except:
                # We need extra exception handling in the case the server fails in mid connection, it's an edge case but I've seen it
                return [("501 Gateway error", [('Content-Type', 'text/html')],), '
Could not connect
']

        def retry_known_hosts(url, environ):
            # retry the given request against all the hosts the current session has run against
            nhosts = copy.copy(initial_forwarding_registry.values())
            nhosts.reverse()
            hosts = [];
            # Can't use set() here because it needs to keep it's original ordering.
            for h in nhosts:
                if h not in hosts and h not in exclude_from_retry: hosts.append(h)
            current_host = url.netloc
            for host in hosts:
                connection = make_remote_connection(urlparse(url.geturl().replace(current_host, host)),
                                                    change_environ_domain(current_host, host, environ))
                if isinstance(connection, HTTPConnection):
                    new_response = connection.getresponse()
                    if new_response.status > 199 and new_response.status < 399:
                        logger.info('Retry success, '+url.geturl()+' to '+host)
                        return new_response
            return None

        connection = make_remote_connection(url, environ)
        if not isinstance(connection, HTTPConnection):
            # if it's not an HTTPConnection object then the request failed so we should retry
            new_response = retry_known_hosts(url, environ)
            if new_response is not None:
                response = new_response
            else:
                start_response(*connection.pop(0))
                return get_wsgi_response(connection.pop(0))
        else:
            response = connection.getresponse()

        if response.status == 404:
            # If the response is 404 we should retry against the known hosts
            # this is usually the case when the referrer is ommitted by the browser
            # for random reasons
            new_response = retry_known_hosts(url, environ)
            if new_response is not None:
                response = new_response

        # Remove hop by hop headers
        headers = self.parse_headers(response)
        if response.status == 404:
            logger.info('Could not fullfill proxy request to '+url.geturl()+'. '+str(set(initial_forwarding_registry.values())))
        start_response(response.status.__str__()+' '+response.reason, headers)
        return get_wsgi_response(response)

    def parse_headers(self, response):
        headers = [ (x.lower(), y,) for x, y in [ z.split(':', 1) for z in str(response.msg).splitlines() if ':' in z]]
        for header in headers:
            if is_hop_by_hop(header[0]):
                headers.remove(header)
            elif header[0] == 'location':
                # There should never be a legitimate redirect to /windmill-serv from a remote site
                if '/windmill-serv' in header[1]:
                    i = headers.index(header)
                    location = header[1]
                    headers.remove(header)
                    headers.insert(i, ('location',location.split('/windmill-serv')[0],))
        return headers

    def __call__(self, environ, start_response):
        return self.handler(environ, start_response)<|endoftext|>