class ProposalForm(forms.ModelForm):
    """
    Form for proposal submission including ``title``, ``text`` and
    ``tags`` fields.
    """

    title = forms.CharField(widget=forms.TextInput(attrs={"class":
                                                          "form-control",
                                                          "id": "title",
                                                          "maxlength": "100",
                                                          "placeholder":
                                                          "Please be"
                                                          " descriptive."}))
    text = forms.CharField(widget=forms.Textarea(attrs={"class":
                                                        "form-control",
                                                        "maxlength": "6000",
                                                        "id": "text",
                                                        "onkeyup":
                                                        "countChars"
                                                        "(this, 6000)",
                                                        "oninput":
                                                        "this.editor.update()"}))
    school_tags = forms.ModelMultipleChoiceField(
        widget=forms.CheckboxSelectMultiple(),
        queryset=Tag.objects.filter(group=1),
        required=False)
    liberation_tags = forms.ModelMultipleChoiceField(
        widget=forms.CheckboxSelectMultiple(),
        queryset=Tag.objects.filter(group=2),
        required=False)
    other_tags = forms.ModelMultipleChoiceField(
        widget=forms.CheckboxSelectMultiple(),
        queryset=Tag.objects.filter(group=3),
        required=False)

    def clean_school_tags(self):
        """
        Get the cleaned school tags if there are no more than
        4 of them

        :returns:   Cleaned school tags of length < 4
        :raises:   :mod:`django.forms.ValidationError`
        """
        cleaned_school_tags = self.cleaned_data["school_tags"]
        if len(cleaned_school_tags) > 4:
            raise forms.ValidationError("Please add no more than "
                                        "four school tags")
        return cleaned_school_tags

    def clean_liberation_tags(self):
        """
        Get the cleaned liberation tags if there are no more than
        4 of them

        :returns:   Cleaned liberation tags of length < 4
        :raises:   :mod:`django.forms.ValidationError`
        """
        cleaned_liberation_tags = self.cleaned_data["liberation_tags"]
        if len(cleaned_liberation_tags) > 4:
            raise forms.ValidationError("Please add no more than "
                                        "four liberation group tags")
        return cleaned_liberation_tags

    def clean_other_tags(self):
        """
        Get the cleaned general tags if there are no more than
        4 of them

        :returns:   Cleaned general tags of length < 4
        :raises:   :mod:`django.forms.ValidationError`
        """
        cleaned_other_tags = self.cleaned_data["other_tags"]
        if len(cleaned_other_tags) > 4:
            raise forms.ValidationError("Please include no more than "
                                        "four general tags")
        return cleaned_other_tags

    def clean_title(self):
        """
        Get the cleaned proposal title if it contains no swear words,
        otherwise raise an excption.

        :returns:  Cleaned ``title`` if it contains no swear words
        :rtype:    string
        :raises:   :mod:`django.forms.ValidationError`
        """
        cleaned_title = self.cleaned_data["title"]
        # don't allow swear words
        if contains_swear_words(cleaned_title):
            raise forms.ValidationError("Proposals cannot contain"
                                        " swear words.")
        return cleaned_title

    def clean_text(self):
        """
        Get the cleaned proposal text if it contains no swear words,
        otherwise raise an exception.

        :returns:  Cleaned ``title`` if it contains no swear words
        :rtype:    string
        :raises:   :mod:`django.forms.ValidationError`
        """
        cleaned_text = self.cleaned_data["text"]
        # don't allow swear words
        if contains_swear_words(cleaned_text):
            raise forms.ValidationError("Proposals cannot contain"
                                        " swear words.")
        return cleaned_text

    class Meta:
        model = Proposal
        fields = ['title', 'text'] class ResponseForm(SwearFilteredModelForm):
    text = forms.CharField(widget=forms.Textarea(
        attrs={"class": "form-control",
               "rows": "3",
               "placeholder": "What does EUSA think of the proposal? What "
                              "will they do about it, or what has been done so far?"}))

    class Meta:
        model = Response
        fields = ['text'] class AmendmentForm(forms.Form):
    """
    Form for suggesting proposal amendments including ``title`` and ``text``
    fields.
    """
    title = forms.CharField(widget=forms.TextInput(attrs={"class":
                                                          "form-control",
                                                          "id": "title",
                                                          "maxlength": "100",
                                                          "placeholder":
                                                          "Please be"
                                                          "descriptive."}))
    text = forms.CharField(widget=forms.Textarea(attrs={"class":
                                                        "form-control",
                                                        "maxlength": "6000",
                                                        "id": "text",
                                                        "onkeyup":
                                                        "countChars"
                                                        "(this, 6000)"}))

    def set_initial(self, proposal_title, proposal_text):
        """
        Set the initial value of form fields before rendering.

        :param proposal_title: Initial title in form
        :type proposal_title:  string
        :param proposal_text:  Initial text in form
        :type proposal_text:   string
        """
        self.initial['title'] = proposal_title
        self.initial['text'] = proposal_text class ProposalStatusForm(forms.ModelForm):
    """
    Form for changing the status of a proposal.
    """
    status = forms.ChoiceField(choices=Proposal.PROPOSAL_STATUS_CHOICES,
                               widget=forms.Select(
                                   attrs={"class": "form-control"}))
    student_council = forms.ModelChoiceField(
        StudentCouncil.objects.filter(datetime__gt=datetime.now()),
        widget=forms.Select(attrs={"class": "form-control"}),
        required=False)

    class Meta:
        model = Proposal
        fields = ["status", "student_council"] class CommentForm(SwearFilteredModelForm):
    text = forms.CharField(widget=forms.Textarea(
        attrs={"class": "form-control",
               "rows": "3",
               "maxlength": "6000",
               "onkeyup": "countChars(this, 6000)"}))

    class Meta:
        model = Comment
        fields = ['text'] class Proposal(Content):
    PROPOSAL_STATUS_CHOICES = (
        (1, "Open for discussion"),
        (2, "Work in progress"),
        (3, "Going to Student Council"),
        (4, "Resolved")
    )
    title = models.CharField(max_length=100)
    text = models.TextField()
    slug = models.SlugField(default="slug", max_length=100)
    tags = models.ManyToManyField(Tag, related_name="proposals")
    status = models.IntegerField(choices=PROPOSAL_STATUS_CHOICES, default=1)
    student_council = models.ForeignKey(StudentCouncil,
                                        related_name="proposals",
                                        null=True)

    objects = ProposalManager()

    def __unicode__(self):
        return self.title

    def __str__(self):
        return self.title

    def get_content_type():
        """
        Gets ContentType of Proposal model, caching result in ``_content_type``
        when first retrieved.

        :returns: ContentType of Proposal model
        :rtype:   :mod:`django.contrib.contenttypes.models.ContentType`
        """
        if not hasattr(Proposal, '_content_type'):
            Proposal._content_type = ContentType.objects.get(
                app_label="proposals", model="proposal")
        return Proposal._content_type

    def save(self, *args, **kwargs):
        """
        Create slug, save proposal and for initial save add
        vote by proposer.
        """
        is_initial = not self.pk
        self.slug = slugify(self.title, max_length=100)
        super(Proposal, self).save(*args, **kwargs)
        # when the proposal is first created, add a vote by the proposer
        if is_initial:
            Vote.objects.create(user=self.user, content=self, isVoteUp=True)

    def get_absolute_url(self):
        """
        Get the proposal's URL using :mod:`django.core.urlresolvers.reverse`
        """
        return reverse("proposal", kwargs={"proposal_id": self.id,
                                           "slug": self.slug})

    def get_votes_up_percentage(self):
        """
        Get percentage of votes which are up votes
        """
        votes_up = self.upVotes
        votes_total = votes_up + self.downVotes
        if votes_total == 0:
            return 0
        else:
            return (votes_up/votes_total) * 100

    def get_votes_down_percentage(self):
        """
        Get percentage of votes which are down votes
        """
        votes_up_percentage = self.get_votes_up_percentage()
        return 100 - votes_up_percentage

    @staticmethod
    def _hours_since(date):
        utc_now = datetime.datetime.utcnow()
        utc_event = datetime.datetime.utcfromtimestamp(date.timestamp())
        return (utc_now - utc_event).total_seconds() / 3600.0

    @staticmethod
    def _weight_instance(hour_age, gravity=1.8):
        return 1 / pow((hour_age+2), gravity)

    @staticmethod
    def _proximity_coefficient():
        return 1

    def get_rank(self):
        rank = 0

        # Take sum of weighted value for each comment
        comments = Comment.objects.filter(proposal=self)
        for comment in comments:
            rank += Proposal._weight_instance(
                    hour_age=Proposal._hours_since(comment.createdAt)) * 4

        votes = self.user.get_votes()
        # Vote.get_votes(self)
        for vote in votes:
            hour_age = Proposal._hours_since(vote.createdAt)
            if vote.isVoteUp:
                rank += Proposal._weight_instance(hour_age) * 2
            else:
                rank += Proposal._weight_instance(hour_age) * 1

        return rank * Proposal._proximity_coefficient()

    def get_visible_comments(self, reply_to=None, sort="popularity"):
        return Comment.objects.get_visible_comments(
            proposal=self, reply_to=reply_to, sort=sort)

    def is_new(self):
        """
        :return: True if the proposal is less than an hour old
        """
        timesince = datetime.datetime.now() - self.createdAt
        return timesince < datetime.timedelta(hours=1) class Response(Content):
    text = models.TextField()
    proposal = models.ForeignKey(Proposal, related_name="responses")

    def save(self, *args, **kwargs):
        if self.user.userStatus == "Staff" or \
           self.user.userStatus == "Officeholder" or \
           self.user == self.proposal.user:
            super(Response, self).save(*args, **kwargs)
        else:
            raise Exception("Only staff, officeholders or the proposer "
                            "can respond to proposals!")

    def __unicode__(self):
        return "%s" % self.text class Comment(Content):
    # The maximum length of 'text' is 6100 because a proposal can have 6000
    # characters in its body and 100 in its title. Because amendments are
    # stored as Comments, they must be at least as long as Proposals.
    text = models.CharField(max_length=6100)
    proposal = models.ForeignKey('proposals.Proposal', null=False,
                                 related_name="comments")
    replyTo = models.ForeignKey("self", null=True)
    isAmendment = models.BooleanField(default=False)
    votes = GenericRelation(Vote)

    objects = CommentManager()

    def get_content_type():
        if not hasattr(Comment, '_content_type'):
            Comment._content_type = ContentType.objects.get(
                app_label="comments", model="comment")
        return Comment._content_type

    def save(self, *args, **kwargs):
        is_initial = not self.pk
        super(Comment, self).save(*args, **kwargs)
        # when the comment is first created, add a vote by the commenter
        if is_initial:
            Vote.objects.create(user=self.user, content=self, isVoteUp=True)

    def is_new(self):
        """
        :return: True if the comment is less than 5 minutes old
        """
        timesince = datetime.datetime.now() - self.createdAt
        return timesince < datetime.timedelta(minutes=5)

    def get_replies(self, sort="popularity"):
        if sort == "popularity":
            return Comment.objects.filter(replyTo=self).filter(isHidden=False)\
                .select_related('user').order_by("rank")
        elif sort == "chronological":
            return Comment.objects.filter(replyTo=self).filter(isHidden=False)\
                .select_related('user').order_by("createdAt")

    def get_score(self):
        return self.upVotes - self.downVotes

    def __unicode__(self):
        return "%s" % self.text class Tag(models.Model):
    TAG_GROUPS = (
        (1, "School"),
        (2, "Liberation"),
        (3, "Other")
    )
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(blank=False, max_length=100)
    group = models.IntegerField(choices=TAG_GROUPS, default=1)
    description = models.TextField(blank=True)

    def save(self, *args, **kwargs):
        self.slug = slugify(self.name, max_length=100)
        super(Tag, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse("tag", kwargs={"tagId": self.id, "slug": self.slug})

    def __unicode__(self):
        return self.name

    def __str__(self):
        return self.name def request_login(request):
    messages.add_message(request, messages.INFO,
                         "You must be logged in to do this")
    return HttpResponseRedirect(reverse('frontpage')) class HideAction(models.Model):
    id = models.AutoField(primary_key=True)
    moderator = models.ForeignKey(settings.AUTH_USER_MODEL)
    createdAt = models.DateTimeField(auto_now_add=True)
    reason = models.CharField(max_length=2000)

    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content = GenericForeignKey()

    def save(self, *args, **kwargs):
        if not self.moderator.isModerator:
            raise Exception("Only moderators may perform hide actions!")
        else:
            super(HideAction, self).save()
            self.content.isHidden = True
            self.content.save()

    @staticmethod
    def get_hide_actions(object_id, content_type):
        return HideAction.objects.filter(content_type=content_type)\
                                 .filter(object_id=object_id) def do_vote(user, content, vote_request):
    try:
        content_type = ContentType.objects.get_for_model(content)
        old_vote = Vote.objects.get(user=user,
                                    object_id=content.id,
                                    content_type=content_type)
        vote_was_up = old_vote.isVoteUp
        old_vote.delete()
        new_vote = False
    except:
        new_vote = True

    vote = Vote(user=user, content=content)

    if new_vote:
        if vote_request == "up":
            vote.isVoteUp = True
            vote.save()
            return 1
        elif vote_request == "down":
            vote.isVoteUp = False
            vote.save()
            return -1

    elif not new_vote:
        if vote_request == "up":
            if vote_was_up:
                return 0
            else:
                vote.isVoteUp = True
                vote.save()
                return 1
        elif vote_request == "down":
            if not vote_was_up:
                return 0
            else:
                vote.isVoteUp = False
                vote.save()
                return -1
    else:
        raise Exception("Unknown vote request " + str(vote_request)) def to_queryset(searchqueryset):
    """
    This function converts a SearchQuerySet into a QuerySet.

    We don't use a generator here because pagination in the API requires
    that you can take the len() of a list, a generators don't have a len().
    """
    return [item.object for item in searchqueryset]
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect,\
    Http404, HttpResponse
from django.shortcuts import render, redirect
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth import get_user_model
from haystack.query import SearchQuerySet
from lxml.html.diff import htmldiff
from itertools import chain
from proposals.forms import ProposalForm, ResponseForm, AmendmentForm,\
    ProposalStatusForm
from comments.forms import CommentForm
from proposals.models import Proposal, Response
from comments.models import Comment
from tags.models import Tag
from users.views import request_login
from moderation.models import HideAction
from votes.views import do_vote
from core.utils import to_queryset

def submit(request):
    if not request.user.is_authenticated():
        return request_login(request)
    tags = Tag.objects.all()
    if request.method == 'POST':  # If the form has been submitted...
        form = ProposalForm(request.POST)  # A form bound to the POST data
        if form.is_valid():  # All validation rules pass
            proposal = form.save(commit=False)
            proposal.user = request.user
            proposal.save()
            tags = list(chain(form.cleaned_data.get("school_tags"),
                              form.cleaned_data.get("liberation_tags"),
                              form.cleaned_data.get("other_tags")))
            proposal.tags.add(*tags)
            return HttpResponseRedirect(
                reverse("share",
                        kwargs={"proposal_id": proposal.id}))
        else:
            errors = form.errors
            return render(request, "submit.html", {"form": form,
                                                   "tags": tags,
                                                   "errors": errors})
    else:
        form = ProposalForm()  # An unbound form
        return render(request, 'submit.html', {'form': form,
                                               "tags": tags})


def proposal(request, proposal_id, slug=None):
    try:
        proposal = Proposal.objects.get(id=proposal_id)
    except:
        raise Http404

    response_headers = dict()

    # redirect requests with the wrong slug to the correct page
    if not slug == proposal.slug:
        return HttpResponsePermanentRedirect(proposal.get_absolute_url())

    user_vote = None
    if request.method == 'POST':  # If the form has been submitted...
        if not request.user.is_authenticated():
            return request_login(request)
        if 'request' in request.POST:
            if request.POST['request'] == 'proposal_vote':
                vote_request = request.POST['vote']
                user_vote = do_vote(request.user, proposal, vote_request)
            elif request.POST['request'] == 'comment_vote':
                vote_string = request.POST['vote']
                # Parse vote
                if vote_string[0:2] == 'up':
                    comment_id = int(vote_string[2:])
                    response_headers['Comment-Id'] = str(comment_id)
                    comment = Comment.objects.get(id=comment_id)
                    do_vote(request.user, comment, 'up')
                elif vote_string[0:4] == 'down':
                    comment_id = int(vote_string[4:])
                    response_headers['Comment-Id'] = str(comment_id)
                    comment = Comment.objects.get(id=comment_id)
                    do_vote(request.user, comment, 'down')
                else:
                    raise Exception('Unknown vote string ' + vote_string)
            else:
                raise Exception('Unknown POST request '
                                + request.POST['request'])
            proposal = Proposal.objects.get(id=proposal_id)
        else:
            form = CommentForm(request.POST)
            comment = form.save(commit=False)
            if 'reply_to' in request.POST and request.POST['reply_to']:
                # Comment is a reply
                comment.replyTo = Comment.objects\
                                         .get(id=request.POST['reply_to'])
                response_headers['Comment-Id'] = request.POST['reply_to']
                response_headers['Is-Reply'] = 'True'
            if form.is_valid():  # All validation rules pass
                # Process the data in form.cleaned_data
                comment.user = request.user
                comment.proposal = proposal
                comment.save()
                if not 'Is-Reply' in response_headers:
                    response_headers['Comment-Id'] = str(comment.id)

    hide = None
    if proposal.isHidden:
        hide = HideAction.objects.get(object_id=proposal.id,
                                      content_type=Proposal.get_content_type())

    if request.user.is_authenticated() and not user_vote:
        user_vote = request.user.get_vote_on(proposal)

    similar_proposals = to_queryset(
        SearchQuerySet().more_like_this(proposal))[:3]
    new_proposals = Proposal.objects.get_visible_proposals()\
                            .exclude(id=proposal_id).order_by("-createdAt")[:3]

    form = CommentForm()
    comments = proposal.get_visible_comments()

    can_write_response = False
    if request.user.is_authenticated():
        if request.user.userStatus == "Staff" or \
           request.user.userStatus == "Officeholder" or \
           request.user == proposal.user:
            can_write_response = True

    context = {"form": form,
               "proposal": proposal,
               "comments": comments,
               "user_vote": user_vote,
               "hide": hide,
               "similar_proposals": similar_proposals,
               "new_proposals": new_proposals,
               "can_write_response": can_write_response}

    comment_votes = {}
    if request.user.is_authenticated():
        comment_votes["comments_voted_for"] = \

            Comment.objects.filter(votes__user=request.user,