<commit_before># -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from decimal import Decimal

from django.db import models
from django.db.models import Q, F
from django.db.models.signals import post_save

from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _

from symposion.markdown_parser import parse
from symposion.proposals.models import ProposalBase
from symposion.schedule.models import Presentation

def score_expression():
    return (
        (2 * F("plus_two") + F("plus_one")) -
        (F("minus_one") + 2 * F("minus_two"))
    ) / (
        F("vote_count") - F("abstain")
    )

class Votes(object):
    ABSTAIN = "0"
    PLUS_TWO = "+2"
    PLUS_ONE = "+1"
    MINUS_ONE = "−1"
    MINUS_TWO = "−2"

    CHOICES = [
        (PLUS_TWO, _("+2 — Good proposal and I will argue for it to be accepted.")),
        (PLUS_ONE, _("+1 — OK proposal, but I will not argue for it to be accepted.")),
        (MINUS_ONE, _("−1 — Weak proposal, but I will not argue strongly against acceptance.")),
        (MINUS_TWO, _("−2 — Serious issues and I will argue to reject this proposal.")),
        (ABSTAIN, _("Abstain - I do not want to review this proposal and I do not want to see it again.")),
    ]
VOTES = Votes()

class ReviewAssignment(models.Model):
    AUTO_ASSIGNED_INITIAL = 0
    OPT_IN = 1
    AUTO_ASSIGNED_LATER = 2

    NUM_REVIEWERS = 3

    ORIGIN_CHOICES = [
        (AUTO_ASSIGNED_INITIAL, _("auto-assigned, initial")),
        (OPT_IN, _("opted-in")),
        (AUTO_ASSIGNED_LATER, _("auto-assigned, later")),
    ]

    proposal = models.ForeignKey(ProposalBase, verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    origin = models.IntegerField(choices=ORIGIN_CHOICES, verbose_name=_("Origin"))

    assigned_at = models.DateTimeField(default=datetime.now, verbose_name=_("Assigned at"))
    opted_out = models.BooleanField(default=False, verbose_name=_("Opted out"))

    @classmethod
    def create_assignments(cls, proposal, origin=AUTO_ASSIGNED_INITIAL):
        speakers = [proposal.speaker] + list(proposal.additional_speakers.all())
        reviewers = User.objects.exclude(
            pk__in=[
                speaker.user_id
                for speaker in speakers
                if speaker.user_id is not None
            ] + [
                assignment.user_id
                for assignment in ReviewAssignment.objects.filter(
                    proposal_id=proposal.id)]
        ).filter(
            groups__name="reviewers",
        ).filter(
            Q(reviewassignment__opted_out=False) | Q(reviewassignment=None)
        ).annotate(
            num_assignments=models.Count("reviewassignment")
        ).order_by(
            "num_assignments", "?",
        )
        num_assigned_reviewers = ReviewAssignment.objects.filter(
            proposal_id=proposal.id, opted_out=0).count()
        for reviewer in reviewers[:max(0, cls.NUM_REVIEWERS - num_assigned_reviewers)]:
            cls._default_manager.create(
                proposal=proposal,
                user=reviewer,
                origin=origin,
            )

class ProposalMessage(models.Model):
    proposal = models.ForeignKey(ProposalBase, related_name="messages", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    message = models.TextField(verbose_name=_("Message"))
    message_html = models.TextField(blank=True)
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    def save(self, *args, **kwargs):
        self.message_html = parse(self.message)
        return super(ProposalMessage, self).save(*args, **kwargs)

    class Meta:
        ordering = ["submitted_at"]
        verbose_name = _("proposal message")
        verbose_name_plural = _("proposal messages")

class Review(models.Model):
    VOTES = VOTES

    proposal = models.ForeignKey(ProposalBase, related_name="reviews", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    # No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
    # like some complicated encoding system.
    vote = models.CharField(max_length=2, blank=True, choices=VOTES.CHOICES, verbose_name=_("Vote"))
    comment = models.TextField(verbose_name=_("Comment"))
    comment_html = models.TextField(blank=True)
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    def save(self, **kwargs):
        self.comment_html = parse(self.comment)
        if self.vote:
            vote, created = LatestVote.objects.get_or_create(
                proposal=self.proposal,
                user=self.user,
                defaults=dict(
                    vote=self.vote,
                    submitted_at=self.submitted_at,
                )
            )
            if not created:
                LatestVote.objects.filter(pk=vote.pk).update(vote=self.vote)
                self.proposal.result.update_vote(self.vote, previous=vote.vote)
            else:
                self.proposal.result.update_vote(self.vote)
        super(Review, self).save(**kwargs)

    def delete(self):
        model = self.__class__
        user_reviews = model._default_manager.filter(
            proposal=self.proposal,
            user=self.user,
        )
        try:
            # find the latest review
            latest = user_reviews.exclude(pk=self.pk).order_by("-submitted_at")[0]
        except IndexError:
            # did not find a latest which means this must be the only one.
            # treat it as a last, but delete the latest vote.
            self.proposal.result.update_vote(self.vote, removal=True)
            lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
            lv.delete()
        else:
            # handle that we've found a latest vote
            # check if self is the lastest vote
            if self == latest:
                # self is the latest review; revert the latest vote to the
                # previous vote
                previous = user_reviews.filter(submitted_at__lt=self.submitted_at)\
                    .order_by("-submitted_at")[0]
                self.proposal.result.update_vote(self.vote, previous=previous.vote, removal=True)
                lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
                lv.update(
                    vote=previous.vote,
                    submitted_at=previous.submitted_at,
                )
            else:
                # self is not the latest review so we just need to decrement
                # the comment count
                self.proposal.result.comment_count = models.F("comment_count") - 1
                self.proposal.result.save()
        # in all cases we need to delete the review; let's do it!
        super(Review, self).delete()

    def css_class(self):
        return {
            self.VOTES.ABSTAIN: "abstain",
            self.VOTES.PLUS_TWO: "plus-two",
            self.VOTES.PLUS_ONE: "plus-one",
            self.VOTES.MINUS_ONE: "minus-one",
            self.VOTES.MINUS_TWO: "minus-two",
        }[self.vote]

    @property
    def section(self):
        return self.proposal.kind.section.slug

    class Meta:
        verbose_name = _("review")
        verbose_name_plural = _("reviews")

class LatestVote(models.Model):
    VOTES = VOTES

    proposal = models.ForeignKey(ProposalBase, related_name="votes", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    # No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
    # like some complicated encoding system.
    vote = models.CharField(max_length=2, choices=VOTES.CHOICES, verbose_name=_("Vote"))
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    class Meta:
        unique_together = [("proposal", "user")]
        verbose_name = _("latest vote")
        verbose_name_plural = _("latest votes")

    def css_class(self):
        return {
            self.VOTES.ABSTAIN: "abstain",
            self.VOTES.PLUS_TWO: "plus-two",
            self.VOTES.PLUS_ONE: "plus-one",
            self.VOTES.MINUS_ONE: "minus-one",
            self.VOTES.MINUS_TWO: "minus-two",
        }[self.vote]

class ProposalResult(models.Model):
    proposal = models.OneToOneField(ProposalBase, related_name="result", verbose_name=_("Proposal"))
    score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"), verbose_name=_("Score"))
    comment_count = models.PositiveIntegerField(default=0, verbose_name=_("Comment count"))
    vote_count = models.PositiveIntegerField(default=0, verbose_name=_("Vote count"))
    abstain = models.PositiveIntegerField(default=0, verbose_name=_("Abstain"))
    plus_two = models.PositiveIntegerField(default=0, verbose_name=_("Plus two"))
    plus_one = models.PositiveIntegerField(default=0, verbose_name=_("Plus one"))
    minus_one = models.PositiveIntegerField(default=0, verbose_name=_("Minus one"))
    minus_two = models.PositiveIntegerField(default=0, verbose_name=_("Minus two"))
    accepted = models.NullBooleanField(choices=[
        (True, "accepted"),
        (False, "rejected"),
        (None, "undecided"),
    ], default=None, verbose_name=_("Accepted"))
    status = models.CharField(max_length=20, choices=[
        ("accepted", _("accepted")),
        ("rejected", _("rejected")),
        ("undecided", _("undecided")),
        ("standby", _("standby")),
    ], default="undecided", verbose_name=_("Status"))

    @classmethod
    def full_calculate(cls):
        for proposal in ProposalBase.objects.all():
            result, created = cls._default_manager.get_or_create(proposal=proposal)
            result.comment_count = Review.objects.filter(proposal=proposal).count()
            result.vote_count = LatestVote.objects.filter(proposal=proposal).count()
            result.abstain = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.ABSTAIN,
            ).count()
            result.plus_two = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.PLUS_TWO
            ).count()
            result.plus_one = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.PLUS_ONE
            ).count()
            result.minus_one = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.MINUS_ONE
            ).count()
            result.minus_two = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.MINUS_TWO
            ).count()
            result.save()
            cls._default_manager.filter(pk=result.pk).update(score=score_expression())

    def update_vote(self, vote, previous=None, removal=False):
        mapping = {
            VOTES.PLUS_TWO: "plus_two",
            VOTES.PLUS_ONE: "plus_one",
            VOTES.MINUS_ONE: "minus_one",
            VOTES.MINUS_TWO: "minus_two",
        }
        if previous:
            if previous == vote:
                return
            if removal:
                setattr(self, mapping[previous], models.F(mapping[previous]) + 1)
            else:
                setattr(self, mapping[previous], models.F(mapping[previous]) - 1)
        else:
            if removal:
                self.vote_count = models.F("vote_count") - 1
            else:
                self.vote_count = models.F("vote_count") + 1
        if removal:
            setattr(self, mapping[vote], models.F(mapping[vote]) - 1)
            self.comment_count = models.F("comment_count") - 1
        else:
            setattr(self, mapping[vote], models.F(mapping[vote]) + 1)
            self.comment_count = models.F("comment_count") + 1
        self.save()
        model = self.__class__
        model._default_manager.filter(pk=self.pk).update(score=score_expression())

    class Meta:
        verbose_name = _("proposal_result")
        verbose_name_plural = _("proposal_results")

class Comment(models.Model):
    proposal = models.ForeignKey(ProposalBase, related_name="comments", verbose_name=_("Proposal"))
    commenter = models.ForeignKey(User, verbose_name=_("Commenter"))
    text = models.TextField(verbose_name=_("Text"))
    text_html = models.TextField(blank=True)

    # Or perhaps more accurately, can the user see this comment.
    public = models.BooleanField(choices=[(True, _("public")), (False, _("private"))], default=False, verbose_name=_("Public"))
    commented_at = models.DateTimeField(default=datetime.now, verbose_name=_("Commented at"))

    class Meta:
        verbose_name = _("comment")
        verbose_name_plural = _("comments")

    def save(self, *args, **kwargs):
        self.text_html = parse(self.text)
        return super(Comment, self).save(*args, **kwargs)

class NotificationTemplate(models.Model):

    label = models.CharField(max_length=100, verbose_name=_("Label"))
    from_address = models.EmailField(verbose_name=_("From address"))
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
    body = models.TextField(verbose_name=_("Body"))

    class Meta:
        verbose_name = _("notification template")
        verbose_name_plural = _("notification templates")

class ResultNotification(models.Model):

    proposal = models.ForeignKey(ProposalBase, related_name="notifications", verbose_name=_("Proposal"))
    template = models.ForeignKey(NotificationTemplate, null=True, blank=True,
                                 on_delete=models.SET_NULL, verbose_name=_("Template"))
    timestamp = models.DateTimeField(default=datetime.now, verbose_name=_("Timestamp"))
    to_address = models.EmailField(verbose_name=_("To address"))
    from_address = models.EmailField(verbose_name=_("From address"))
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
    body = models.TextField(verbose_name=_("Body"))

    def recipients(self):
        for speaker in self.proposal.speakers():
            yield speaker.email

    @property
    def email_args(self):
        return (self.subject, self.body, self.from_address, self.recipients())

def promote_proposal(proposal):
    if hasattr(proposal, "presentation") and proposal.presentation:
        # already promoted
        presentation = proposal.presentation
    else:
        presentation = Presentation(
            title=proposal.title,
            description=proposal.description,
            abstract=proposal.abstract,
            speaker=proposal.speaker,
            section=proposal.section,
            proposal_base=proposal,
        )
        presentation.save()
        for speaker in proposal.additional_speakers.all():
            presentation.additional_speakers.add(speaker)
            presentation.save()

    return presentation

def unpromote_proposal(proposal):
    if hasattr(proposal, "presentation") and proposal.presentation:
        proposal.presentation.delete()

def accepted_proposal(sender, instance=None, **kwargs):
    if instance is None:
        return
    if instance.status == "accepted":
        promote_proposal(instance.proposal)
    else:
        unpromote_proposal(instance.proposal)
post_save.connect(accepted_proposal, sender=ProposalResult)
<commit_msg>More changes that make abstention work<commit_after># -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from decimal import Decimal

from django.db import models
from django.db.models import Q, F
from django.db.models.signals import post_save

from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _

from symposion.markdown_parser import parse
from symposion.proposals.models import ProposalBase
from symposion.schedule.models import Presentation

def score_expression():
    return (
        (2 * F("plus_two") + F("plus_one")) -
        (F("minus_one") + 2 * F("minus_two"))
    ) / (
        F("vote_count") - F("abstain") * 1.0
    )

class Votes(object):
    ABSTAIN = "0"
    PLUS_TWO = "+2"
    PLUS_ONE = "+1"
    MINUS_ONE = "−1"
    MINUS_TWO = "−2"

    CHOICES = [
        (PLUS_TWO, _("+2 — Good proposal and I will argue for it to be accepted.")),
        (PLUS_ONE, _("+1 — OK proposal, but I will not argue for it to be accepted.")),
        (MINUS_ONE, _("−1 — Weak proposal, but I will not argue strongly against acceptance.")),
        (MINUS_TWO, _("−2 — Serious issues and I will argue to reject this proposal.")),
        (ABSTAIN, _("Abstain - I do not want to review this proposal and I do not want to see it again.")),
    ]
VOTES = Votes()

class ReviewAssignment(models.Model):
    AUTO_ASSIGNED_INITIAL = 0
    OPT_IN = 1
    AUTO_ASSIGNED_LATER = 2

    NUM_REVIEWERS = 3

    ORIGIN_CHOICES = [
        (AUTO_ASSIGNED_INITIAL, _("auto-assigned, initial")),
        (OPT_IN, _("opted-in")),
        (AUTO_ASSIGNED_LATER, _("auto-assigned, later")),
    ]

    proposal = models.ForeignKey(ProposalBase, verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    origin = models.IntegerField(choices=ORIGIN_CHOICES, verbose_name=_("Origin"))

    assigned_at = models.DateTimeField(default=datetime.now, verbose_name=_("Assigned at"))
    opted_out = models.BooleanField(default=False, verbose_name=_("Opted out"))

    @classmethod
    def create_assignments(cls, proposal, origin=AUTO_ASSIGNED_INITIAL):
        speakers = [proposal.speaker] + list(proposal.additional_speakers.all())
        reviewers = User.objects.exclude(
            pk__in=[
                speaker.user_id
                for speaker in speakers
                if speaker.user_id is not None
            ] + [
                assignment.user_id
                for assignment in ReviewAssignment.objects.filter(
                    proposal_id=proposal.id)]
        ).filter(
            groups__name="reviewers",
        ).filter(
            Q(reviewassignment__opted_out=False) | Q(reviewassignment=None)
        ).annotate(
            num_assignments=models.Count("reviewassignment")
        ).order_by(
            "num_assignments", "?",
        )
        num_assigned_reviewers = ReviewAssignment.objects.filter(
            proposal_id=proposal.id, opted_out=0).count()
        for reviewer in reviewers[:max(0, cls.NUM_REVIEWERS - num_assigned_reviewers)]:
            cls._default_manager.create(
                proposal=proposal,
                user=reviewer,
                origin=origin,
            )

class ProposalMessage(models.Model):
    proposal = models.ForeignKey(ProposalBase, related_name="messages", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    message = models.TextField(verbose_name=_("Message"))
    message_html = models.TextField(blank=True)
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    def save(self, *args, **kwargs):
        self.message_html = parse(self.message)
        return super(ProposalMessage, self).save(*args, **kwargs)

    class Meta:
        ordering = ["submitted_at"]
        verbose_name = _("proposal message")
        verbose_name_plural = _("proposal messages")

class Review(models.Model):
    VOTES = VOTES

    proposal = models.ForeignKey(ProposalBase, related_name="reviews", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    # No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
    # like some complicated encoding system.
    vote = models.CharField(max_length=2, blank=True, choices=VOTES.CHOICES, verbose_name=_("Vote"))
    comment = models.TextField(verbose_name=_("Comment"))
    comment_html = models.TextField(blank=True)
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    def save(self, **kwargs):
        self.comment_html = parse(self.comment)
        if self.vote:
            vote, created = LatestVote.objects.get_or_create(
                proposal=self.proposal,
                user=self.user,
                defaults=dict(
                    vote=self.vote,
                    submitted_at=self.submitted_at,
                )
            )
            if not created:
                LatestVote.objects.filter(pk=vote.pk).update(vote=self.vote)
                self.proposal.result.update_vote(self.vote, previous=vote.vote)
            else:
                self.proposal.result.update_vote(self.vote)
        super(Review, self).save(**kwargs)

    def delete(self):
        model = self.__class__
        user_reviews = model._default_manager.filter(
            proposal=self.proposal,
            user=self.user,
        )
        try:
            # find the latest review
            latest = user_reviews.exclude(pk=self.pk).order_by("-submitted_at")[0]
        except IndexError:
            # did not find a latest which means this must be the only one.
            # treat it as a last, but delete the latest vote.
            self.proposal.result.update_vote(self.vote, removal=True)
            lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
            lv.delete()
        else:
            # handle that we've found a latest vote
            # check if self is the lastest vote
            if self == latest:
                # self is the latest review; revert the latest vote to the
                # previous vote
                previous = user_reviews.filter(submitted_at__lt=self.submitted_at)\
                    .order_by("-submitted_at")[0]
                self.proposal.result.update_vote(self.vote, previous=previous.vote, removal=True)
                lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
                lv.update(
                    vote=previous.vote,
                    submitted_at=previous.submitted_at,
                )
            else:
                # self is not the latest review so we just need to decrement
                # the comment count
                self.proposal.result.comment_count = models.F("comment_count") - 1
                self.proposal.result.save()
        # in all cases we need to delete the review; let's do it!
        super(Review, self).delete()

    def css_class(self):
        return {
            self.VOTES.ABSTAIN: "abstain",
            self.VOTES.PLUS_TWO: "plus-two",
            self.VOTES.PLUS_ONE: "plus-one",
            self.VOTES.MINUS_ONE: "minus-one",
            self.VOTES.MINUS_TWO: "minus-two",
        }[self.vote]

    @property
    def section(self):
        return self.proposal.kind.section.slug

    class Meta:
        verbose_name = _("review")
        verbose_name_plural = _("reviews")

class LatestVote(models.Model):
    VOTES = VOTES

    proposal = models.ForeignKey(ProposalBase, related_name="votes", verbose_name=_("Proposal"))
    user = models.ForeignKey(User, verbose_name=_("User"))

    # No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
    # like some complicated encoding system.
    vote = models.CharField(max_length=2, choices=VOTES.CHOICES, verbose_name=_("Vote"))
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))

    class Meta:
        unique_together = [("proposal", "user")]
        verbose_name = _("latest vote")
        verbose_name_plural = _("latest votes")

    def css_class(self):
        return {
            self.VOTES.ABSTAIN: "abstain",
            self.VOTES.PLUS_TWO: "plus-two",
            self.VOTES.PLUS_ONE: "plus-one",
            self.VOTES.MINUS_ONE: "minus-one",
            self.VOTES.MINUS_TWO: "minus-two",
        }[self.vote]

class ProposalResult(models.Model):
    proposal = models.OneToOneField(ProposalBase, related_name="result", verbose_name=_("Proposal"))
    score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"), verbose_name=_("Score"))
    comment_count = models.PositiveIntegerField(default=0, verbose_name=_("Comment count"))
    vote_count = models.PositiveIntegerField(default=0, verbose_name=_("Vote count"))
    abstain = models.PositiveIntegerField(default=0, verbose_name=_("Abstain"))
    plus_two = models.PositiveIntegerField(default=0, verbose_name=_("Plus two"))
    plus_one = models.PositiveIntegerField(default=0, verbose_name=_("Plus one"))
    minus_one = models.PositiveIntegerField(default=0, verbose_name=_("Minus one"))
    minus_two = models.PositiveIntegerField(default=0, verbose_name=_("Minus two"))
    accepted = models.NullBooleanField(choices=[
        (True, "accepted"),
        (False, "rejected"),
        (None, "undecided"),
    ], default=None, verbose_name=_("Accepted"))
    status = models.CharField(max_length=20, choices=[
        ("accepted", _("accepted")),
        ("rejected", _("rejected")),
        ("undecided", _("undecided")),
        ("standby", _("standby")),
    ], default="undecided", verbose_name=_("Status"))

    @classmethod
    def full_calculate(cls):
        for proposal in ProposalBase.objects.all():
            result, created = cls._default_manager.get_or_create(proposal=proposal)
            result.comment_count = Review.objects.filter(proposal=proposal).count()
            result.vote_count = LatestVote.objects.filter(proposal=proposal).count()
            result.abstain = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.ABSTAIN,
            ).count()
            result.plus_two = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.PLUS_TWO
            ).count()
            result.plus_one = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.PLUS_ONE
            ).count()
            result.minus_one = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.MINUS_ONE
            ).count()
            result.minus_two = LatestVote.objects.filter(
                proposal=proposal,
                vote=VOTES.MINUS_TWO
            ).count()
            result.save()
            cls._default_manager.filter(pk=result.pk).update(score=score_expression())

    def update_vote(self, vote, previous=None, removal=False):
        mapping = {
            VOTES.ABSTAIN: "abstain",
            VOTES.PLUS_TWO: "plus_two",
            VOTES.PLUS_ONE: "plus_one",
            VOTES.MINUS_ONE: "minus_one",
            VOTES.MINUS_TWO: "minus_two",
        }
        if previous:
            if previous == vote:
                return
            if removal:
                setattr(self, mapping[previous], models.F(mapping[previous]) + 1)
            else:
                setattr(self, mapping[previous], models.F(mapping[previous]) - 1)
        else:
            if removal:
                self.vote_count = models.F("vote_count") - 1
            else:
                self.vote_count = models.F("vote_count") + 1
        if removal:
            setattr(self, mapping[vote], models.F(mapping[vote]) - 1)
            self.comment_count = models.F("comment_count") - 1
        else:
            setattr(self, mapping[vote], models.F(mapping[vote]) + 1)
            self.comment_count = models.F("comment_count") + 1
        self.save()
        model = self.__class__
        model._default_manager.filter(pk=self.pk).update(score=score_expression())

    class Meta:
        verbose_name = _("proposal_result")
        verbose_name_plural = _("proposal_results")

class Comment(models.Model):
    proposal = models.ForeignKey(ProposalBase, related_name="comments", verbose_name=_("Proposal"))
    commenter = models.ForeignKey(User, verbose_name=_("Commenter"))
    text = models.TextField(verbose_name=_("Text"))
    text_html = models.TextField(blank=True)

    # Or perhaps more accurately, can the user see this comment.
    public = models.BooleanField(choices=[(True, _("public")), (False, _("private"))], default=False, verbose_name=_("Public"))
    commented_at = models.DateTimeField(default=datetime.now, verbose_name=_("Commented at"))

    class Meta:
        verbose_name = _("comment")
        verbose_name_plural = _("comments")

    def save(self, *args, **kwargs):
        self.text_html = parse(self.text)
        return super(Comment, self).save(*args, **kwargs)

class NotificationTemplate(models.Model):

    label = models.CharField(max_length=100, verbose_name=_("Label"))
    from_address = models.EmailField(verbose_name=_("From address"))
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
    body = models.TextField(verbose_name=_("Body"))

    class Meta:
        verbose_name = _("notification template")
        verbose_name_plural = _("notification templates")

class ResultNotification(models.Model):

    proposal = models.ForeignKey(ProposalBase, related_name="notifications", verbose_name=_("Proposal"))
    template = models.ForeignKey(NotificationTemplate, null=True, blank=True,
                                 on_delete=models.SET_NULL, verbose_name=_("Template"))
    timestamp = models.DateTimeField(default=datetime.now, verbose_name=_("Timestamp"))
    to_address = models.EmailField(verbose_name=_("To address"))
    from_address = models.EmailField(verbose_name=_("From address"))
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
    body = models.TextField(verbose_name=_("Body"))

    def recipients(self):
        for speaker in self.proposal.speakers():
            yield speaker.email

    @property
    def email_args(self):
        return (self.subject, self.body, self.from_address, self.recipients())

def promote_proposal(proposal):
    if hasattr(proposal, "presentation") and proposal.presentation:
        # already promoted
        presentation = proposal.presentation
    else:
        presentation = Presentation(
            title=proposal.title,
            description=proposal.description,
            abstract=proposal.abstract,
            speaker=proposal.speaker,
            section=proposal.section,
            proposal_base=proposal,
        )
        presentation.save()
        for speaker in proposal.additional_speakers.all():
            presentation.additional_speakers.add(speaker)
            presentation.save()

    return presentation

def unpromote_proposal(proposal):
    if hasattr(proposal, "presentation") and proposal.presentation:
        proposal.presentation.delete()

def accepted_proposal(sender, instance=None, **kwargs):
    if instance is None:
        return
    if instance.status == "accepted":
        promote_proposal(instance.proposal)
    else:
        unpromote_proposal(instance.proposal)
post_save.connect(accepted_proposal, sender=ProposalResult)
<|endoftext|>


<commit_before>#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 logging
import os
import threading

import grpc

from apache_beam import coders
from apache_beam.internal import pickler
from apache_beam.portability import common_urns
from apache_beam.portability.api import beam_job_api_pb2
from apache_beam.portability.api import beam_job_api_pb2_grpc
from apache_beam.runners import pipeline_context
from apache_beam.runners import runner

TERMINAL_STATES = [
    beam_job_api_pb2.JobState.DONE,
    beam_job_api_pb2.JobState.STOPPED,
    beam_job_api_pb2.JobState.FAILED,
    beam_job_api_pb2.JobState.CANCELLED,
]

class PortableRunner(runner.PipelineRunner):
  """
    Experimental: No backward compatibility guaranteed.
    A BeamRunner that executes Python pipelines via the Beam Job API.

    This runner is a stub and does not run the actual job.
    This runner schedules the job on a job service. The responsibility of
    running and managing the job lies with the job service used.
  """

  # TODO(angoenka): Read all init parameters from pipeline_options.
  def __init__(self,
               runner_api_address=None,
               job_service_address=None,
               docker_image=None):
    super(PortableRunner, self).__init__()

    self._subprocess = None
    self._runner_api_address = runner_api_address
    if not job_service_address:
      raise ValueError(
          'job_service_address should be provided while creating runner.')
    self._job_service_address = job_service_address
    self._docker_image = docker_image or self.default_docker_image()

  @staticmethod
  def default_docker_image():
    if 'USER' in os.environ:
      # Perhaps also test if this was built?
      logging.info('Using latest locally built Python SDK docker image.')
      return os.environ['USER'] + '-docker-apache.bintray.io/beam/python:latest'
    else:
      logging.warning('Could not find a Python SDK docker image.')
      return 'unknown'

  def _create_job_service(self):
    return beam_job_api_pb2_grpc.JobServiceStub(
        grpc.insecure_channel(self._job_service_address))

  def run_pipeline(self, pipeline):
    # Java has different expectations about coders
    # (windowed in Fn API, but *un*windowed in runner API), whereas the
    # FnApiRunner treats them consistently, so we must guard this.
    # See also BEAM-2717.
    proto_context = pipeline_context.PipelineContext(
        default_environment_url=self._docker_image)
    proto_pipeline = pipeline.to_runner_api(context=proto_context)
    if self._runner_api_address:
      for pcoll in proto_pipeline.components.pcollections.values():
        if pcoll.coder_id not in proto_context.coders:
          coder = coders.registry.get_coder(pickler.loads(pcoll.coder_id))
          pcoll.coder_id = proto_context.coders.get_id(coder)
      proto_context.coders.populate_map(proto_pipeline.components.coders)

    # Some runners won't detect the GroupByKey transform unless it has no
    # subtransforms.  Remove all sub-transforms until BEAM-4605 is resolved.
    for _, transform_proto in list(
        proto_pipeline.components.transforms.items()):
      if transform_proto.spec.urn == common_urns.primitives.GROUP_BY_KEY.urn:
        for sub_transform in transform_proto.subtransforms:
          del proto_pipeline.components.transforms[sub_transform]
        del transform_proto.subtransforms[:]

    job_service = self._create_job_service()
    prepare_response = job_service.Prepare(
        beam_job_api_pb2.PrepareJobRequest(
            job_name='job', pipeline=proto_pipeline))
    run_response = job_service.Run(
        beam_job_api_pb2.RunJobRequest(
            preparation_id=prepare_response.preparation_id))
    return PipelineResult(job_service, run_response.job_id)

class PipelineResult(runner.PipelineResult):

  def __init__(self, job_service, job_id):
    super(PipelineResult, self).__init__(beam_job_api_pb2.JobState.UNSPECIFIED)
    self._job_service = job_service
    self._job_id = job_id
    self._messages = []

  def cancel(self):
    self._job_service.Cancel()

  @property
  def state(self):
    runner_api_state = self._job_service.GetState(
        beam_job_api_pb2.GetJobStateRequest(job_id=self._job_id)).state
    self._state = self._runner_api_state_to_pipeline_state(runner_api_state)
    return self._state

  @staticmethod
  def _runner_api_state_to_pipeline_state(runner_api_state):
    return getattr(runner.PipelineState,
                   beam_job_api_pb2.JobState.Enum.Name(runner_api_state))

  @staticmethod
  def _pipeline_state_to_runner_api_state(pipeline_state):
    return beam_job_api_pb2.JobState.Enum.Value(pipeline_state)

  def wait_until_finish(self):

    def read_messages():
      for message in self._job_service.GetMessageStream(
          beam_job_api_pb2.JobMessagesRequest(job_id=self._job_id)):
        self._messages.append(message)

    t = threading.Thread(target=read_messages, name='wait_until_finish_read')
    t.daemon = True
    t.start()

    for state_response in self._job_service.GetStateStream(
        beam_job_api_pb2.GetJobStateRequest(job_id=self._job_id)):
      self._state = self._runner_api_state_to_pipeline_state(
          state_response.state)
      if state_response.state in TERMINAL_STATES:
        break
    if self._state != runner.PipelineState.DONE:
      raise RuntimeError(
          'Pipeline %s failed in state %s.' % (self._job_id, self._state))
    return self._state
<commit_msg>Stage empty manifest if a staging session is given.<commit_after>#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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 logging
import os
import threading

import grpc

from apache_beam import coders
from apache_beam.internal import pickler
from apache_beam.portability import common_urns
from apache_beam.portability.api import beam_artifact_api_pb2
from apache_beam.portability.api import beam_artifact_api_pb2_grpc
from apache_beam.portability.api import beam_job_api_pb2
from apache_beam.portability.api import beam_job_api_pb2_grpc
from apache_beam.runners import pipeline_context
from apache_beam.runners import runner

TERMINAL_STATES = [
    beam_job_api_pb2.JobState.DONE,
    beam_job_api_pb2.JobState.STOPPED,
    beam_job_api_pb2.JobState.FAILED,
    beam_job_api_pb2.JobState.CANCELLED,
]

class PortableRunner(runner.PipelineRunner):
  """
    Experimental: No backward compatibility guaranteed.
    A BeamRunner that executes Python pipelines via the Beam Job API.

    This runner is a stub and does not run the actual job.
    This runner schedules the job on a job service. The responsibility of
    running and managing the job lies with the job service used.
  """

  # TODO(angoenka): Read all init parameters from pipeline_options.
  def __init__(self,
               runner_api_address=None,
               job_service_address=None,
               docker_image=None):
    super(PortableRunner, self).__init__()

    self._subprocess = None
    self._runner_api_address = runner_api_address
    if not job_service_address:
      raise ValueError(
          'job_service_address should be provided while creating runner.')
    self._job_service_address = job_service_address
    self._docker_image = docker_image or self.default_docker_image()

  @staticmethod
  def default_docker_image():
    if 'USER' in os.environ:
      # Perhaps also test if this was built?
      logging.info('Using latest locally built Python SDK docker image.')
      return os.environ['USER'] + '-docker-apache.bintray.io/beam/python:latest'
    else:
      logging.warning('Could not find a Python SDK docker image.')
      return 'unknown'

  def _create_job_service(self):
    return beam_job_api_pb2_grpc.JobServiceStub(
        grpc.insecure_channel(self._job_service_address))

  def run_pipeline(self, pipeline):
    # Java has different expectations about coders
    # (windowed in Fn API, but *un*windowed in runner API), whereas the
    # FnApiRunner treats them consistently, so we must guard this.
    # See also BEAM-2717.
    proto_context = pipeline_context.PipelineContext(
        default_environment_url=self._docker_image)
    proto_pipeline = pipeline.to_runner_api(context=proto_context)
    if self._runner_api_address:
      for pcoll in proto_pipeline.components.pcollections.values():
        if pcoll.coder_id not in proto_context.coders:
          coder = coders.registry.get_coder(pickler.loads(pcoll.coder_id))
          pcoll.coder_id = proto_context.coders.get_id(coder)
      proto_context.coders.populate_map(proto_pipeline.components.coders)

    # Some runners won't detect the GroupByKey transform unless it has no
    # subtransforms.  Remove all sub-transforms until BEAM-4605 is resolved.
    for _, transform_proto in list(
        proto_pipeline.components.transforms.items()):
      if transform_proto.spec.urn == common_urns.primitives.GROUP_BY_KEY.urn:
        for sub_transform in transform_proto.subtransforms:
          del proto_pipeline.components.transforms[sub_transform]
        del transform_proto.subtransforms[:]

    job_service = self._create_job_service()
    prepare_response = job_service.Prepare(
        beam_job_api_pb2.PrepareJobRequest(
            job_name='job', pipeline=proto_pipeline))
    if prepare_response.artifact_staging_endpoint.url:
      # Must commit something to get a retrieval token,
      # committing empty manifest for now.
      # TODO(BEAM-3883): Actually stage required files.
      artifact_service = beam_artifact_api_pb2_grpc.ArtifactStagingServiceStub(
          grpc.insecure_channel(prepare_response.artifact_staging_endpoint.url))
      commit_manifest = artifact_service.CommitManifest(
          beam_artifact_api_pb2.CommitManifestRequest(
              manifest=beam_artifact_api_pb2.Manifest(),
              staging_session_token=prepare_response.staging_session_token))
      retrieval_token = commit_manifest.retrieval_token
    else:
      retrieval_token = None
    run_response = job_service.Run(
        beam_job_api_pb2.RunJobRequest(
            preparation_id=prepare_response.preparation_id,
            retrieval_token=retrieval_token))
    return PipelineResult(job_service, run_response.job_id)

class PipelineResult(runner.PipelineResult):

  def __init__(self, job_service, job_id):
    super(PipelineResult, self).__init__(beam_job_api_pb2.JobState.UNSPECIFIED)
    self._job_service = job_service
    self._job_id = job_id
    self._messages = []

  def cancel(self):
    self._job_service.Cancel()

  @property
  def state(self):
    runner_api_state = self._job_service.GetState(
        beam_job_api_pb2.GetJobStateRequest(job_id=self._job_id)).state
    self._state = self._runner_api_state_to_pipeline_state(runner_api_state)
    return self._state

  @staticmethod
  def _runner_api_state_to_pipeline_state(runner_api_state):
    return getattr(runner.PipelineState,
                   beam_job_api_pb2.JobState.Enum.Name(runner_api_state))

  @staticmethod
  def _pipeline_state_to_runner_api_state(pipeline_state):
    return beam_job_api_pb2.JobState.Enum.Value(pipeline_state)

  def wait_until_finish(self):

    def read_messages():
      for message in self._job_service.GetMessageStream(
          beam_job_api_pb2.JobMessagesRequest(job_id=self._job_id)):
        self._messages.append(message)

    t = threading.Thread(target=read_messages, name='wait_until_finish_read')
    t.daemon = True
    t.start()

    for state_response in self._job_service.GetStateStream(
        beam_job_api_pb2.GetJobStateRequest(job_id=self._job_id)):
      self._state = self._runner_api_state_to_pipeline_state(
          state_response.state)
      if state_response.state in TERMINAL_STATES:
        break
    if self._state != runner.PipelineState.DONE:
      raise RuntimeError(
          'Pipeline %s failed in state %s.' % (self._job_id, self._state))
    return self._state
<|endoftext|>


<commit_before>"""

  fs.wrapfs:  class for wrapping an existing FS object with added functionality

This module provides the class WrapFS, a base class for objects that wrap
another FS object and provide some transformation of its contents.  It could
be very useful for implementing e.g. transparent encryption or compression
services.

As a simple example of how this class could be used, the 'HideDotFiles' class
implements the standard unix shell functionality of hiding dot files in
directory listings.

"""

from fnmatch import fnmatch

from fs.base import FS, threading, synchronize
from fs.errors import *

def rewrite_errors(func):
    @wraps(func)
    def wrapper(self,*args,**kwds):
        try:
            return func(self,*args,**kwds)
        except ResourceError, e:
            try:
                e.path = self._decode(e.path)
            except AttributeError:
                pass
            raise
    return wrapper

class WrapFS(FS):
    """FS that wraps another FS, providing translation etc.

    This class allows simple transforms to be applied to the names
    and/or contents of files in an FS.  It could be used to implement
    e.g. compression or encryption in a relatively painless manner.

    The following methods can be overridden to control how files are
    accessed in the underlying FS object:

        _file_wrap(file,mode):  called for each file that is opened from
                                the underlying FS; may return a modified
                                file-like object.

        _encode(path):  encode a path for access in the underlying FS

        _decode(path):  decode a path from the underlying FS

    If the required path translation proceeds one component at a time,
    it may be simpler to override the _encode_name() and _decode_name()
    methods.
    """

    def __init__(self,fs):
        super(WrapFS,self).__init__()
        try:
            self._lock = fs._lock
        except (AttributeError,FSError):
            self._lock = None
        self.wrapped_fs = fs

    def _file_wrap(self,f,mode):
        """Apply wrapping to an opened file."""
        return f

    def _encode_name(self,name):
        """Encode path component for the underlying FS."""
        return name

    def _decode_name(self,name):
        """Decode path component from the underlying FS."""
        return name

    def _encode(self,path):
        """Encode path for the underlying FS."""
        names = path.split("/")
        e_names = []
        for name in names:
            if name == "":
                e_names.append("")
            else:
                e_names.append(self._encode_name(name))
        return "/".join(e_names)

    def _decode(self,path):
        """Decode path from the underlying FS."""
        names = path.split("/")
        d_names = []
        for name in names:
            if name == "":
                d_names.append("")
            else:
                d_names.append(self._decode_name(name))
        return "/".join(d_names)

    def _adjust_mode(self,mode):
        """Adjust the mode used to open a file in the underlying FS.

        This method takes the mode given when opening a file, and should
        return a two-tuple giving the mode to be used in this FS as first
        item, and the mode to be used in the underlying FS as the second.

        An example of why this is needed is a WrapFS subclass that does
        transparent file compression - in this case files from the wrapped
        FS cannot be opened in append mode.
        """
        return (mode,mode)

    @rewrite_errors
    def getsyspath(self,path,allow_none=False):
        return self.wrapped_fs.getsyspath(self._encode(path),allow_none)

    @rewrite_errors
    def hassyspath(self,path):
        return self.wrapped_fs.hassyspath(self._encode(path))

    @rewrite_errors
    def open(self,path,mode="r"):
        (mode,wmode) = self._adjust_mode(mode)
        f = self.wrapped_fs.open(self._encode(path),wmode)
        return self._file_wrap(f,mode)

    @rewrite_errors
    def exists(self,path):
        return self.wrapped_fs.exists(self._encode(path))

    @rewrite_errors
    def isdir(self,path):
        return self.wrapped_fs.isdir(self._encode(path))

    @rewrite_errors
    def isfile(self,path):
        return self.wrapped_fs.isfile(self._encode(path))

    @rewrite_errors
    def listdir(self,path="",**kwds):
        wildcard = kwds.pop("wildcard","*")
        info = kwds.get("info",False)
        entries = []
        for e in self.wrapped_fs.listdir(self._encode(path),**kwds):
            if info:
                e = e.copy()
                e["name"] = self._decode(e["name"])
                if wildcard is not None and not fnmatch(e["name"],wildcard):
                    continue
            else:
                e = self._decode(e)
                if wildcard is not None and not fnmatch(e,wildcard):
                    continue
            entries.append(e)
        return entries

    @rewrite_errors
    def makedir(self,path,*args,**kwds):
        return self.wrapped_fs.makedir(self._encode(path),*args,**kwds)

    @rewrite_errors
    def remove(self,path):
        return self.wrapped_fs.remove(self._encode(path))

    @rewrite_errors
    def removedir(self,path,*args,**kwds):
        return self.wrapped_fs.removedir(self._encode(path),*args,**kwds)

    @rewrite_errors
    def rename(self,src,dst):
        return self.wrapped_fs.rename(self._encode(src),self._encode(dst))

    @rewrite_errors
    def getinfo(self,path):
        return self.wrapped_fs.getinfo(self._encode(path))

    @rewrite_errors
    def desc(self,path):
        return self.wrapped_fs.desc(self._encode(path))

    @rewrite_errors
    def copy(self,src,dst,overwrite=False,chunk_size=16384):
        return self.wrapped_fs.copy(self._encode(src),self._encode(dst),overwrite,chunk_size)

    @rewrite_errors
    def move(self,src,dst,overwrite=False,chunk_size=16384):
        return self.wrapped_fs.move(self._encode(src),self._encode(dst),overwrite,chunk_size)

    @rewrite_errors
    def movedir(self,src,dst,overwrite=False,ignore_errors=False,chunk_size=16384):
        return self.wrapped_fs.movedir(self._encode(src),self._encode(dst),overwrite,ignore_errors,chunk_size)

    @rewrite_errors
    def copydir(self,src,dst,overwrite=False,ignore_errors=False,chunk_size=16384):
        return self.wrapped_fs.copydir(self._encode(src),self._encode(dst),overwrite,ignore_errors,chunk_size)

    @rewrite_errors
    def getxattr(self,path,name,default=None):
        return self.wrapped_fs.getxattr(self._encode(path),name,default)

    @rewrite_errors
    def setxattr(self,path,name,value):
        return self.wrapped_fs.setxattr(self._encode(path),name,value)

    @rewrite_errors
    def delxattr(self,path,name):
        return self.wrapped_fs.delxattr(self._encode(path),name)

    @rewrite_errors
    def listxattrs(self,path):
        return self.wrapped_fs.listxattrs(self._encode(path))

    def __getattr__(self,attr):
        return getattr(self.wrapped_fs,attr)

    @rewrite_errors
    def close(self):
        if hasattr(self.wrapped_fs,"close"):
            self.wrapped_fs.close()

def wrap_fs_methods(decorator,cls=None):
    """Apply the given decorator to all FS methods on the given class.

    This function can be used in two ways.  When called with two arguments it
    applies the given function 'decorator' to each FS method of the given
    class.  When called with just a single argument, it creates and returns
    a class decorator which will do the same thing when applied.  So you can
    use it like this:

        wrap_fs_methods(mydecorator,MyFSClass)

    Or on more recent Python versions, like this:

        @wrap_fs_methods(mydecorator)
        class MyFSClass(FS):
            ...

    """
    methods = ("open","exists","isdir","isfile","listdir","makedir","remove",
               "removedir","rename","getinfo","copy","move","copydir",
               "movedir","close","getxattr","setxattr","delxattr","listxattrs")
    def apply_decorator(cls):
        for method_name in methods:
            method = getattr(cls,method_name,None)
            if method is not None:
                setattr(cls,method_name,decorator(method))
        return cls
    if cls is not None:
        return apply_decorator(cls)
    else:
        return apply_decorator

class HideDotFiles(WrapFS):
    """FS wrapper class that hides dot-files in directory listings.

    The listdir() function takes an extra keyword argument 'hidden'
    indicating whether hidden dotfiles shoud be included in the output.
    It is False by default.
    """

    def is_hidden(self,path):
        """Check whether the given path should be hidden."""
        return path and basename(path)[0] == "."

    def _encode(self,path):
        return path

    def _decode(self,path):
        return path

    def listdir(self,path="",**kwds):
        hidden = kwds.pop("hidden",True)
        entries = self.wrapped_fs.listdir(path,**kwds)
        if not hidden:
            entries = [e for e in entries if not self.is_hidden(e)]
        return entries

    def walk(self, path="/", wildcard=None, dir_wildcard=None, search="breadth",hidden=False):
        if search == "breadth":
            dirs = [path]
            while dirs:
                current_path = dirs.pop()
                paths = []
                for filename in self.listdir(current_path,hidden=hidden):
                    path = pathjoin(current_path, filename)
                    if self.isdir(path):
                        if dir_wildcard is not None:
                            if fnmatch(path, dir_wilcard):
                                dirs.append(path)
                        else:
                            dirs.append(path)
                    else:
                        if wildcard is not None:
                            if fnmatch(path, wildcard):
                                paths.append(filename)
                        else:
                            paths.append(filename)
                yield (current_path, paths)
        elif search == "depth":
            def recurse(recurse_path):
                for path in self.listdir(recurse_path, wildcard=dir_wildcard, full=True, dirs_only=True,hidden=hidden):
                    for p in recurse(path):
                        yield p
                yield (recurse_path, self.listdir(recurse_path, wildcard=wildcard, files_only=True,hidden=hidden))
            for p in recurse(path):
                yield p
        else:
            raise ValueError("Search should be 'breadth' or 'depth'")

    def isdirempty(self, path):
        path = normpath(path)
        iter_dir = iter(self.listdir(path,hidden=True))
        try:
            iter_dir.next()
        except StopIteration:
            return True
        return False

class LimitSize(WrapFS):

    def __init__(self,fs,max_size):
        self.max_size = max_size
        super(LimitSize,self).__init__(fs)
        self.cur_size = sum(self.getsize(f) for f in self.walkfiles())

    @synchronize
    def open(self,path,mode="r"):
        try:
            size = self.getsize(path)
        except ResourceNotFoundError:
            size = 0
        f = super(LimitSize,self).open(path,mode)
        if "w" in mode:
            self.cur_size -= size
            size = 0
        return LimitSizeFile(self,f,mode,size)

    def _acquire_space(self,size):
        new_size = self.cur_size + size
        if new_size > self.max_size:
            raise StorageSpaceError("write")
        self.cur_size = new_size

    #  Disable use of system-level paths, so that copy/copydir have to
    #  fall back to manual writes and pass through _acquire_space.
    getsyspath = FS.getsyspath
    copy = FS.copy
    copydir = FS.copydir

    @synchronize
    def remove(self,path):
        size = self.getsize(path)
        super(LimitSize,self).remove(path)
        self.cur_size -= size

    @synchronize
    def removedir(self,path,recursive=False,force=False):
        size = sum(self.getsize(f) for f in self.walkfiles(path))
        super(LimitSize,self).removedir(path,recursive=recursive,force=force)
        self.cur_size -= size

class LimitSizeFile(object):

    def __init__(self,fs,file,mode,size):
        self._lock = fs._lock
        self.fs = fs
        self.file = file
        self.mode = mode
        self.size = size
        self.closed = False

    @synchronize
    def write(self,data):
        pos = self.file.tell()
        if pos > self.size:
            self.size = pos
        diff = pos + len(data) - self.size
        if diff <= 0:
            self.file.write(data)
        else:
            self.fs._acquire_space(diff)
            self.file.write(data)
            self.size += diff

    def writelines(self,lines):
        for line in lines:
            self.write(line)

    @synchronize
    def truncate(self,size=None):
        pos = self.file.tell()
        if size is None:
            size = pos
        self.fs._acquire_space(size - self.size)
        self.file.truncate(size)
        self.size = size

    #  This is lifted straight from the stdlib's tempfile.py
    def __getattr__(self,name):
        file = self.__dict__['file']
        a = getattr(file, name)
        if not issubclass(type(a), type(0)):
            setattr(self, name, a)
        return a

    def __enter__(self):
        self.file.__enter__()
        return self

    def __exit__(self,exc,value,tb):
        self.close()
        return False

    def __iter__(self):
        return iter(self.file)

<commit_msg>fix keyword arguments in WrapFS<commit_after>"""

  fs.wrapfs:  class for wrapping an existing FS object with added functionality

This module provides the class WrapFS, a base class for objects that wrap
another FS object and provide some transformation of its contents.  It could
be very useful for implementing e.g. transparent encryption or compression
services.

As a simple example of how this class could be used, the 'HideDotFiles' class
implements the standard unix shell functionality of hiding dot files in
directory listings.

"""

from fnmatch import fnmatch

from fs.base import FS, threading, synchronize
from fs.errors import *

def rewrite_errors(func):
    @wraps(func)
    def wrapper(self,*args,**kwds):
        try:
            return func(self,*args,**kwds)
        except ResourceError, e:
            try:
                e.path = self._decode(e.path)
            except AttributeError:
                pass
            raise
    return wrapper

class WrapFS(FS):
    """FS that wraps another FS, providing translation etc.

    This class allows simple transforms to be applied to the names
    and/or contents of files in an FS.  It could be used to implement
    e.g. compression or encryption in a relatively painless manner.

    The following methods can be overridden to control how files are
    accessed in the underlying FS object:

        _file_wrap(file,mode):  called for each file that is opened from
                                the underlying FS; may return a modified
                                file-like object.

        _encode(path):  encode a path for access in the underlying FS

        _decode(path):  decode a path from the underlying FS

    If the required path translation proceeds one component at a time,
    it may be simpler to override the _encode_name() and _decode_name()
    methods.
    """

    def __init__(self,fs):
        super(WrapFS,self).__init__()
        try:
            self._lock = fs._lock
        except (AttributeError,FSError):
            self._lock = None
        self.wrapped_fs = fs

    def _file_wrap(self,f,mode):
        """Apply wrapping to an opened file."""
        return f

    def _encode_name(self,name):
        """Encode path component for the underlying FS."""
        return name

    def _decode_name(self,name):
        """Decode path component from the underlying FS."""
        return name

    def _encode(self,path):
        """Encode path for the underlying FS."""
        names = path.split("/")
        e_names = []
        for name in names:
            if name == "":
                e_names.append("")
            else:
                e_names.append(self._encode_name(name))
        return "/".join(e_names)

    def _decode(self,path):
        """Decode path from the underlying FS."""
        names = path.split("/")
        d_names = []
        for name in names:
            if name == "":
                d_names.append("")
            else:
                d_names.append(self._decode_name(name))
        return "/".join(d_names)

    def _adjust_mode(self,mode):
        """Adjust the mode used to open a file in the underlying FS.

        This method takes the mode given when opening a file, and should
        return a two-tuple giving the mode to be used in this FS as first
        item, and the mode to be used in the underlying FS as the second.

        An example of why this is needed is a WrapFS subclass that does
        transparent file compression - in this case files from the wrapped
        FS cannot be opened in append mode.
        """
        return (mode,mode)

    @rewrite_errors
    def getsyspath(self,path,allow_none=False):
        return self.wrapped_fs.getsyspath(self._encode(path),allow_none)

    @rewrite_errors
    def hassyspath(self,path):
        return self.wrapped_fs.hassyspath(self._encode(path))

    @rewrite_errors
    def open(self,path,mode="r"):
        (mode,wmode) = self._adjust_mode(mode)
        f = self.wrapped_fs.open(self._encode(path),wmode)
        return self._file_wrap(f,mode)

    @rewrite_errors
    def exists(self,path):
        return self.wrapped_fs.exists(self._encode(path))

    @rewrite_errors
    def isdir(self,path):
        return self.wrapped_fs.isdir(self._encode(path))

    @rewrite_errors
    def isfile(self,path):
        return self.wrapped_fs.isfile(self._encode(path))

    @rewrite_errors
    def listdir(self,path="",**kwds):
        wildcard = kwds.pop("wildcard","*")
        info = kwds.get("info",False)
        entries = []
        for e in self.wrapped_fs.listdir(self._encode(path),**kwds):
            if info:
                e = e.copy()
                e["name"] = self._decode(e["name"])
                if wildcard is not None and not fnmatch(e["name"],wildcard):
                    continue
            else:
                e = self._decode(e)
                if wildcard is not None and not fnmatch(e,wildcard):
                    continue
            entries.append(e)
        return entries

    @rewrite_errors
    def makedir(self,path,*args,**kwds):
        return self.wrapped_fs.makedir(self._encode(path),*args,**kwds)

    @rewrite_errors
    def remove(self,path):
        return self.wrapped_fs.remove(self._encode(path))

    @rewrite_errors
    def removedir(self,path,*args,**kwds):
        return self.wrapped_fs.removedir(self._encode(path),*args,**kwds)

    @rewrite_errors
    def rename(self,src,dst):
        return self.wrapped_fs.rename(self._encode(src),self._encode(dst))

    @rewrite_errors
    def getinfo(self,path):
        return self.wrapped_fs.getinfo(self._encode(path))

    @rewrite_errors
    def desc(self,path):
        return self.wrapped_fs.desc(self._encode(path))

    @rewrite_errors
    def copy(self,src,dst,**kwds):
        return self.wrapped_fs.copy(self._encode(src),self._encode(dst),**kwds)

    @rewrite_errors
    def move(self,src,dst,**kwds):
        return self.wrapped_fs.move(self._encode(src),self._encode(dst),**kwds)

    @rewrite_errors
    def movedir(self,src,dst,**kwds):
        return self.wrapped_fs.movedir(self._encode(src),self._encode(dst),**kwds)

    @rewrite_errors
    def copydir(self,src,dst,**kwds):
        return self.wrapped_fs.copydir(self._encode(src),self._encode(dst),**kwds)

    @rewrite_errors
    def getxattr(self,path,name,default=None):
        return self.wrapped_fs.getxattr(self._encode(path),name,default)

    @rewrite_errors
    def setxattr(self,path,name,value):
        return self.wrapped_fs.setxattr(self._encode(path),name,value)

    @rewrite_errors
    def delxattr(self,path,name):
        return self.wrapped_fs.delxattr(self._encode(path),name)

    @rewrite_errors
    def listxattrs(self,path):
        return self.wrapped_fs.listxattrs(self._encode(path))

    def __getattr__(self,attr):
        return getattr(self.wrapped_fs,attr)

    @rewrite_errors
    def close(self):
        if hasattr(self.wrapped_fs,"close"):
            self.wrapped_fs.close()

def wrap_fs_methods(decorator,cls=None):
    """Apply the given decorator to all FS methods on the given class.

    This function can be used in two ways.  When called with two arguments it
    applies the given function 'decorator' to each FS method of the given
    class.  When called with just a single argument, it creates and returns
    a class decorator which will do the same thing when applied.  So you can
    use it like this:

        wrap_fs_methods(mydecorator,MyFSClass)

    Or on more recent Python versions, like this:

        @wrap_fs_methods(mydecorator)
        class MyFSClass(FS):
            ...

    """
    methods = ("open","exists","isdir","isfile","listdir","makedir","remove",
               "removedir","rename","getinfo","copy","move","copydir",
               "movedir","close","getxattr","setxattr","delxattr","listxattrs")
    def apply_decorator(cls):
        for method_name in methods:
            method = getattr(cls,method_name,None)
            if method is not None:
                setattr(cls,method_name,decorator(method))
        return cls
    if cls is not None:
        return apply_decorator(cls)
    else:
        return apply_decorator

class HideDotFiles(WrapFS):
    """FS wrapper class that hides dot-files in directory listings.

    The listdir() function takes an extra keyword argument 'hidden'
    indicating whether hidden dotfiles shoud be included in the output.
    It is False by default.
    """

    def is_hidden(self,path):
        """Check whether the given path should be hidden."""
        return path and basename(path)[0] == "."

    def _encode(self,path):
        return path

    def _decode(self,path):
        return path

    def listdir(self,path="",**kwds):
        hidden = kwds.pop("hidden",True)
        entries = self.wrapped_fs.listdir(path,**kwds)
        if not hidden:
            entries = [e for e in entries if not self.is_hidden(e)]
        return entries

    def walk(self, path="/", wildcard=None, dir_wildcard=None, search="breadth",hidden=False):
        if search == "breadth":
            dirs = [path]
            while dirs:
                current_path = dirs.pop()
                paths = []
                for filename in self.listdir(current_path,hidden=hidden):
                    path = pathjoin(current_path, filename)
                    if self.isdir(path):
                        if dir_wildcard is not None:
                            if fnmatch(path, dir_wilcard):
                                dirs.append(path)
                        else:
                            dirs.append(path)
                    else:
                        if wildcard is not None:
                            if fnmatch(path, wildcard):
                                paths.append(filename)
                        else:
                            paths.append(filename)
                yield (current_path, paths)
        elif search == "depth":
            def recurse(recurse_path):
                for path in self.listdir(recurse_path, wildcard=dir_wildcard, full=True, dirs_only=True,hidden=hidden):
                    for p in recurse(path):
                        yield p
                yield (recurse_path, self.listdir(recurse_path, wildcard=wildcard, files_only=True,hidden=hidden))
            for p in recurse(path):
                yield p
        else:
            raise ValueError("Search should be 'breadth' or 'depth'")

    def isdirempty(self, path):
        path = normpath(path)
        iter_dir = iter(self.listdir(path,hidden=True))
        try:
            iter_dir.next()
        except StopIteration:
            return True
        return False

class LimitSize(WrapFS):

    def __init__(self,fs,max_size):
        self.max_size = max_size
        super(LimitSize,self).__init__(fs)
        self.cur_size = sum(self.getsize(f) for f in self.walkfiles())

    @synchronize
    def open(self,path,mode="r"):
        try:
            size = self.getsize(path)
        except ResourceNotFoundError:
            size = 0
        f = super(LimitSize,self).open(path,mode)
        if "w" in mode:
            self.cur_size -= size
            size = 0
        return LimitSizeFile(self,f,mode,size)

    def _acquire_space(self,size):
        new_size = self.cur_size + size
        if new_size > self.max_size:
            raise StorageSpaceError("write")
        self.cur_size = new_size

    #  Disable use of system-level paths, so that copy/copydir have to
    #  fall back to manual writes and pass through _acquire_space.
    getsyspath = FS.getsyspath
    copy = FS.copy
    copydir = FS.copydir

    @synchronize
    def remove(self,path):
        size = self.getsize(path)
        super(LimitSize,self).remove(path)
        self.cur_size -= size

    @synchronize
    def removedir(self,path,recursive=False,force=False):
        size = sum(self.getsize(f) for f in self.walkfiles(path))
        super(LimitSize,self).removedir(path,recursive=recursive,force=force)
        self.cur_size -= size

class LimitSizeFile(object):

    def __init__(self,fs,file,mode,size):
        self._lock = fs._lock
        self.fs = fs
        self.file = file
        self.mode = mode
        self.size = size
        self.closed = False

    @synchronize
    def write(self,data):
        pos = self.file.tell()
        if pos > self.size:
            self.size = pos
        diff = pos + len(data) - self.size
        if diff <= 0:
            self.file.write(data)
        else:
            self.fs._acquire_space(diff)
            self.file.write(data)
            self.size += diff

    def writelines(self,lines):
        for line in lines:
            self.write(line)

    @synchronize
    def truncate(self,size=None):
        pos = self.file.tell()
        if size is None:
            size = pos
        self.fs._acquire_space(size - self.size)
        self.file.truncate(size)
        self.size = size

    #  This is lifted straight from the stdlib's tempfile.py
    def __getattr__(self,name):
        file = self.__dict__['file']
        a = getattr(file, name)
        if not issubclass(type(a), type(0)):
            setattr(self, name, a)
        return a

    def __enter__(self):
        self.file.__enter__()
        return self

    def __exit__(self,exc,value,tb):
        self.close()
        return False

    def __iter__(self):
        return iter(self.file)

<|endoftext|>


<commit_before>from django.shortcuts import get_object_or_404
from django.views.generic import dates, list as list_

from elephantblog.models import Category, Entry
from elephantblog.utils import entry_list_lookup_related

try:
    from towel import paginator
except ImportError:
    from django.core import paginator

class ApplicationContentInheritanceMixin(object):
    """
    This mixin autodetects whether the blog is integrated through an
    ApplicationContent and automatically switches to inheritance2.0
    if that's the case.

    This requires at least FeinCMS v1.5.
    """

    def render_to_response(self, context, **response_kwargs):
        if 'app_config' in getattr(self.request, '_feincms_extra_context', {}):
            return self.get_template_names(), context

        return super(ApplicationContentInheritanceMixin, self).render_to_response(
            context, **response_kwargs)

class ListView(ApplicationContentInheritanceMixin, list_.ListView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10

class YearArchiveView(ApplicationContentInheritanceMixin, dates.YearArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    date_field = 'published_on'
    make_object_list = True

class MonthArchiveView(ApplicationContentInheritanceMixin, dates.MonthArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

class DayArchiveView(ApplicationContentInheritanceMixin, dates.DayArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

class DateDetailView(ApplicationContentInheritanceMixin, dates.DateDetailView):
    queryset = Entry.objects.active()
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        response = self.prepare()
        if response:
            return response

        response = self.render_to_response(self.get_context_data(object=self.object))
        return self.finalize(response)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def prepare(self):
        """
        Prepare / pre-process content types. If this method returns anything,
        it is treated as a ``HttpResponse`` and handed back to the visitor.
        """

        response = self.object.setup_request(self.request)
        if response:
            return response

        http404 = None     # store eventual Http404 exceptions for re-raising,
                           # if no content type wants to handle the current self.request
        successful = False # did any content type successfully end processing?

        for content in self.object.content.all_of_type(tuple(self.object._feincms_content_types_with_process)):
            try:
                r = content.process(self.request, view=self)
                if r in (True, False):
                    successful = r
                elif r:
                    return r
            except Http404, e:
                http404 = e

        if not successful:
            if http404:
                # re-raise stored Http404 exception
                raise http404

            """ XXX This does not make sense in this context, does it?
            if not settings.FEINCMS_ALLOW_EXTRA_PATH and \
                    self.request._feincms_extra_context['extra_path'] != '/':
                raise Http404
            """

    def finalize(self, response):
        """
        Runs finalize() on content types having such a method, adds headers and
        returns the final response.
        """

        for content in self.object.content.all_of_type(tuple(self.object._feincms_content_types_with_finalize)):
            r = content.finalize(self.request, response)
            if r:
                return r

        self.object.finalize_response(self.request, response)

        # Add never cache headers in case frontend editing is active
        if hasattr(self.request, "session") and self.request.session.get('frontend_editing', False):
            add_never_cache_headers(response)

        return response

class CategoryListView(ApplicationContentInheritanceMixin, list_.ListView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    template_name_suffix = '_archive_category'

    def get_queryset(self):
        self.category = get_object_or_404(Category, translations__slug=self.kwargs['slug'])

        queryset = super(CategoryListView, self).get_queryset()
        return queryset.filter(categories=self.category)

    def get_context_data(self, **kwargs):
        return super(CategoryListView, self).get_context_data(
            category=self.category,
            **kwargs)
<commit_msg>Remove copy-pasted page-specific snippets from detail view<commit_after>from django.shortcuts import get_object_or_404
from django.views.generic import dates, list as list_

from elephantblog.models import Category, Entry
from elephantblog.utils import entry_list_lookup_related

try:
    from towel import paginator
except ImportError:
    from django.core import paginator

class ApplicationContentInheritanceMixin(object):
    """
    This mixin autodetects whether the blog is integrated through an
    ApplicationContent and automatically switches to inheritance2.0
    if that's the case.

    This requires at least FeinCMS v1.5.
    """

    def render_to_response(self, context, **response_kwargs):
        if 'app_config' in getattr(self.request, '_feincms_extra_context', {}):
            return self.get_template_names(), context

        return super(ApplicationContentInheritanceMixin, self).render_to_response(
            context, **response_kwargs)

class ListView(ApplicationContentInheritanceMixin, list_.ListView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10

class YearArchiveView(ApplicationContentInheritanceMixin, dates.YearArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    date_field = 'published_on'
    make_object_list = True

class MonthArchiveView(ApplicationContentInheritanceMixin, dates.MonthArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

class DayArchiveView(ApplicationContentInheritanceMixin, dates.DayArchiveView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

class DateDetailView(ApplicationContentInheritanceMixin, dates.DateDetailView):
    queryset = Entry.objects.active()
    paginator_class = paginator.Paginator
    paginate_by = 10
    month_format = '%m'
    date_field = 'published_on'

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        response = self.prepare()
        if response:
            return response

        response = self.render_to_response(self.get_context_data(object=self.object))
        return self.finalize(response)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)

    def prepare(self):
        """
        Prepare / pre-process content types. If this method returns anything,
        it is treated as a ``HttpResponse`` and handed back to the visitor.
        """

        http404 = None     # store eventual Http404 exceptions for re-raising,
                           # if no content type wants to handle the current self.request
        successful = False # did any content type successfully end processing?

        for content in self.object.content.all_of_type(tuple(self.object._feincms_content_types_with_process)):
            try:
                r = content.process(self.request, view=self)
                if r in (True, False):
                    successful = r
                elif r:
                    return r
            except Http404, e:
                http404 = e

        if not successful:
            if http404:
                # re-raise stored Http404 exception
                raise http404

            """ XXX This does not make sense in this context, does it?
            if not settings.FEINCMS_ALLOW_EXTRA_PATH and \
                    self.request._feincms_extra_context['extra_path'] != '/':
                raise Http404
            """

    def finalize(self, response):
        """
        Runs finalize() on content types having such a method, adds headers and
        returns the final response.
        """

        for content in self.object.content.all_of_type(tuple(self.object._feincms_content_types_with_finalize)):
            r = content.finalize(self.request, response)
            if r:
                return r

        # Add never cache headers in case frontend editing is active
        if hasattr(self.request, "session") and self.request.session.get('frontend_editing', False):
            add_never_cache_headers(response)

        return response

class CategoryListView(ApplicationContentInheritanceMixin, list_.ListView):
    queryset = Entry.objects.active().transform(entry_list_lookup_related)
    template_name_suffix = '_archive_category'

    def get_queryset(self):
        self.category = get_object_or_404(Category, translations__slug=self.kwargs['slug'])

        queryset = super(CategoryListView, self).get_queryset()
        return queryset.filter(categories=self.category)

    def get_context_data(self, **kwargs):
        return super(CategoryListView, self).get_context_data(
            category=self.category,
            **kwargs)
<|endoftext|>