class Favorite(models.Model):
    thread = models.ForeignKey("Thread", related_name="favorites")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="favorites")

    def __unicode__(self):
        return u"%s is a favorite of %s" % (
            unicode(self.thread), unicode(self.user)) class LastView(models.Model):
    thread = models.ForeignKey("Thread", related_name="lastviews")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="lastviews")
    view_date = models.DateTimeField(auto_now=True)

    def __unicode__(self):
        """Unicode representation"""
        return u"Last view of %s by %s was %s" % (
            unicode(self.thread), unicode(self.user),
            self.view_date.isoformat())

    def num_unread(self):
        if self.thread.date_active.replace(tzinfo=utc) <= self.view_date:
            return 0 # avoid the expensive query below
        else:
            return self.thread.emails.filter(date__gt=self.view_date).count() class MailingList(models.Model):
    """
    An archived mailing-list.
    """
    name = models.CharField(max_length=254, primary_key=True)
    display_name = models.CharField(max_length=255)
    description = models.TextField()
    subject_prefix = models.CharField(max_length=255)
    archive_policy = models.IntegerField(
        choices=[ (p.value, p.name) for p in ArchivePolicy ],
        default=ArchivePolicy.public.value)
    created_at = models.DateTimeField(default=now)

    MAILMAN_ATTRIBUTES = (
        "display_name", "description", "subject_prefix",
        "archive_policy", "created_at",
    )

    @property
    def is_private(self):
        return self.archive_policy == ArchivePolicy.private.value

    @property
    def is_new(self):
        return self.created_at and \
                now() - self.created_at <= datetime.timedelta(days=30)

    def get_recent_dates(self):
        today = now()
        #today -= datetime.timedelta(days=400) #debug
        # the upper boundary is excluded in the search, add one day
        end_date = today + datetime.timedelta(days=1)
        begin_date = end_date - datetime.timedelta(days=32)
        return begin_date, end_date

    def get_participants_count_between(self, begin_date, end_date):
        # We filter on emails dates instead of threads dates because that would
        # also include last month's participants when threads carry from one
        # month to the next
        # TODO: caching?
        return self.emails.filter(
                date__gte=begin_date, date__lt=end_date
            ).values("sender_id").distinct().count()

    def get_threads_between(self, begin_date, end_date):
        return self.threads.filter(
                starting_email__date__lt=end_date,
                date_active__gte=begin_date
            ).order_by("-date_active")

    @property
    def recent_participants_count(self):
        begin_date, end_date = self.get_recent_dates()
        return cache.get_or_set(
            "MailingList:%s:recent_participants_count" % self.name,
            lambda: self.get_participants_count_between(begin_date, end_date),
            3600 * 6) # 6 hours

    @property
    def recent_threads(self):
        begin_date, end_date = self.get_recent_dates()
        return cache.get_or_set(
            "MailingList:%s:recent_threads" % self.name,
            lambda: self.get_threads_between(begin_date, end_date),
            3600 * 6) # 6 hours

    def get_participants_count_for_month(self, year, month):
        def _get_value():
            begin_date = datetime.datetime(year, month, 1, tzinfo=utc)
            end_date = begin_date + datetime.timedelta(days=32)
            end_date = end_date.replace(day=1)
            return self.get_participants_count_between(begin_date, end_date)
        return cache.get_or_set(
            "MailingList:%s:p_count_for:%s:%s" % (self.name, year, month),
            _get_value, None)

    @property
    def top_posters(self):
        begin_date, end_date = self.get_recent_dates()
        query = Sender.objects.filter(
            emails__mailinglist=self,
            emails__date__gte=begin_date,
            emails__date__lt=end_date,
            ).annotate(count=models.Count("emails")
            ).order_by("-count")
        # Because of South, ResultSets are not pickleizable directly, they must
        # be converted to lists (there's an extra field without the _deferred
        # attribute that causes tracebacks)
        return cache.get_or_set(
            "MailingList:%s:top_posters" % self.name,
            lambda: list(query[:5]),
            3600 * 6) # 6 hours
        # It's not actually necessary to convert back to instances since it's
        # only used in templates where access to instance attributes or
        # dictionnary keys is identical
        #return [ Sender.objects.get(address=data["address"]) for data in sender_ids ]
        #return sender_ids

    def update_from_mailman(self):
        try:
            client = get_mailman_client()
            mm_list = client.get_list(self.name)
        except MailmanConnectionError:
            return
        except HTTPError:
            return # can't update at this time
        if not mm_list:
            return
        converters = {
            "created_at": dateutil.parser.parse,
            "archive_policy": lambda p: ArchivePolicy[p].value,
        }
        for propname in self.MAILMAN_ATTRIBUTES:
            try:
                value = getattr(mm_list, propname)
            except AttributeError:
                value = mm_list.settings[propname]
            if propname in converters:
                value = converters[propname](value)
            setattr(self, propname, value)
        self.save() class Sender(models.Model):
    address = models.EmailField(max_length=255, primary_key=True)
    name = models.CharField(max_length=255)
    mailman_id = models.CharField(max_length=255, null=True, db_index=True)

    def set_mailman_id(self):
        try:
            client = get_mailman_client()
            mm_user = client.get_user(self.address)
        except HTTPError as e:
            raise MailmanConnectionError(e) # normalize all possible error types
        self.mailman_id = mm_user.user_id
        self.save()
        ## Go further and associate the user's other addresses?
        #Sender.objects.filter(address__in=mm_user.addresses
        #    ).update(mailman_id=mm_user.user_id) class Email(models.Model):
    """
    An archived email, from a mailing-list. It is identified by both the list
    name and the message id.
    """
    mailinglist = models.ForeignKey("MailingList", related_name="emails")
    message_id = models.CharField(max_length=255, db_index=True)
    message_id_hash = models.CharField(max_length=255, db_index=True)
    sender = models.ForeignKey("Sender", related_name="emails")
    subject = models.CharField(max_length="512", db_index=True)
    content = models.TextField()
    date = models.DateTimeField(db_index=True)
    timezone = models.SmallIntegerField()
    in_reply_to = models.CharField(
        max_length=255, null=True, blank=True, db_index=True)
    parent = models.ForeignKey("self",
        blank=True, null=True, on_delete=models.SET_NULL,
        related_name="children")
    thread = models.ForeignKey("Thread", related_name="emails")
    archived_date = models.DateTimeField(auto_now_add=True, db_index=True)
    thread_depth = models.IntegerField(default=0)
    thread_order = models.IntegerField(default=0, db_index=True)

    class Meta:
        unique_together = ("mailinglist", "message_id")


    def get_votes(self):
        return get_votes(self)

    def vote(self, value, user):
        # Checks if the user has already voted for this message.
        existing = self.votes.filter(user=user).first()
        if existing is not None and existing.value == value:
            return # Vote already recorded (should I raise an exception?)
        if value not in (0, 1, -1):
            raise ValueError("A vote can only be +1 or -1 (or 0 to cancel)")
        if existing is not None:
            # vote changed or cancelled
            if value == 0:
                existing.delete()
            else:
                existing.value = value
                existing.save()
        else:
            # new vote
            vote = Vote(email=self, user=user, value=value)
            vote.save()

    def set_parent(self, parent):
        if self.id == parent.id:
            raise ValueError("An email can't be its own parent")
        # Compute the subthread
        subthread = [self]
        def _collect_children(current_email):
            children = list(current_email.children.all())
            if not children:
                return
            subthread.extend(children)
            for child in children:
                _collect_children(child)
        _collect_children(self)
        # now set my new parent value
        old_parent_id = self.parent_id
        self.parent = parent
        self.save(update_fields=["parent_id"])
        # If my future parent is in my current subthread, I need to set its
        # parent to my current parent
        if parent in subthread:
            parent.parent_id = old_parent_id
            parent.save(update_fields=["parent_id"])
            # do it after setting the new parent_id to avoid having two
            # parent_ids set to None at the same time (IntegrityError)
        if self.thread_id != parent.thread_id:
            # we changed the thread, reattach the subthread
            former_thread = self.thread
            for child in subthread:
                child.thread = parent.thread
                child.save(update_fields=["thread_id"])
                if child.date > parent.thread.date_active:
                    parent.thread.date_active = child.date
            parent.thread.save()
            # if we were the starting email, or former thread may be empty
            if former_thread.emails.count() == 0:
                former_thread.delete()
        compute_thread_order_and_depth(parent.thread) class Vote(models.Model):
    """
    A User's vote on a message
    """
    email = models.ForeignKey("Email", related_name="votes")
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="votes")
    value = models.SmallIntegerField(db_index=True)

    class Meta:
        unique_together = ("email", "user") class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                related_name="hyperkitty_profile")

    karma = models.IntegerField(default=1)
    TIMEZONES = [ (tz, tz) for tz in pytz.common_timezones ]
    timezone = models.CharField(max_length=100, choices=TIMEZONES, default=u"")

    def __unicode__(self):
        return u'%s' % (unicode(self.user))

    @property
    def emails(self):
        return Email.objects.filter(
            sender__address__in=self.addresses).order_by("date")

    @property
    def addresses(self):
        addresses = set([self.user.email,])
        mm_user = self.get_mailman_user()
        if mm_user:
            # TODO: caching?
            addresses.update(mm_user.addresses)
        return list(sorted(addresses))

    def get_votes_in_list(self, list_name):
        # TODO: Caching ?
        votes = self.user.votes.filter(email__mailinglist__name=list_name)
        likes = votes.filter(value=1).count()
        dislikes = votes.filter(value=-1).count()
        return likes, dislikes

    def get_mailman_user(self):
        # Only cache the user_id, not the whole user instance, because
        # mailmanclient is not pickle-safe
        cache_key = "User:%s:mailman_user_id" % self.id
        user_id = cache.get(cache_key)
        try:
            mm_client = get_mailman_client()
            if user_id is None:
                mm_user = mm_client.get_user(self.user.email)
                cache.set(cache_key, mm_user.user_id, None)
                return mm_user
            else:
                return mm_client.get_user(user_id)
        except (HTTPError, MailmanConnectionError):
            return None

    def get_mailman_user_id(self):
        mm_user = self.get_mailman_user()
        if mm_user is None:
            return None
        return mm_user.user_id

    def get_subscriptions(self):
        def _get_value():
            mm_user = self.get_mailman_user()
            if mm_user is None:
                return []
            mm_client = get_mailman_client()
            sub_names = set()
            for mlist_id in mm_user.subscription_list_ids:
                mlist_name = mm_client.get_list(mlist_id).fqdn_listname
                ## de-duplicate subscriptions
                #if mlist_name in [ s["list_name"] for s in sub_names ]:
                #    continue
                sub_names.add(mlist_name)
            return list(sorted(sub_names))
        # TODO: how should this be invalidated? Subscribe to a signal in
        # mailman when a new subscription occurs? Or store in the session?
        return cache.get_or_set(
            "User:%s:subscriptions" % self.id,
            _get_value, 60 * 10) # 10 minutes

    def get_first_post(self, mlist):
        return self.emails.filter(mailinglist=mlist
            ).order_by("archived_date").first() FLASH_MESSAGES = {
    "updated-ok": ("success", "The profile was successfully updated."),
    "sent-ok": ("success", "The message has been sent successfully."),
    "attached-ok": ("success", "Thread successfully re-attached."),
} def is_mlist_authorized(request, mlist):
    if not mlist.is_private:
        return True
    if not request.user.is_authenticated():
        return False
    # Private list and logged-in user: check subscriptions
    try:
        profile = Profile.objects.get(user_id=request.user.id)
    except Profile.DoesNotExist:
        # Create the profile if it does not exist. There's a signal receiver
        # that creates it for new users, but HyperKitty may be added to an
        # existing Django project with existing users.
        profile = Profile.objects.create(user=request.user)
    if mlist.name in profile.get_subscriptions():
        return True
    else:
        return False def paginate(objects=None, page_num=1, max_page_range=10, paginator=None,
             results_per_page=10):
    if objects is None and paginator is None:
        raise TypeError("You must either provide an 'objects' argument "
                        "or a 'paginator' argument")
    try:
        page_num = int(page_num)
    except (TypeError, ValueError):
        page_num = 1
    if paginator is None:
        # else use the provided instance
        paginator = Paginator(objects, results_per_page)
    try:
        objects = paginator.page(page_num)
    except PageNotAnInteger:
        # If page is not an integer, deliver first page.
        objects = paginator.page(1)
    except EmptyPage:
        # If page is out of range (e.g. 9999), deliver last page of results.
        objects = paginator.page(paginator.num_pages)
    except InvalidPage:
        raise Http404("No such page of results!")
    # Calculate the displayed page range
    if paginator.num_pages > max_page_range:
        objects.page_range = [ 1 ]
        subrange_lower = page_num - int(max_page_range / 2 - 2)
        if subrange_lower > 3:
            objects.page_range.append("...")
        else:
            subrange_lower = 2
        objects.page_range.extend(range(subrange_lower, page_num))
        if page_num != 1 and page_num != 100:
            objects.page_range.append(page_num)
        subrange_upper = page_num + int(max_page_range / 2 - 2)
        if subrange_upper >= paginator.num_pages - 2:
            subrange_upper = paginator.num_pages - 1
        objects.page_range.extend(range(page_num+1, subrange_upper+1))
        if subrange_upper < paginator.num_pages - 2:
            objects.page_range.append("...")
        objects.page_range.append(paginator.num_pages)
    else:
        objects.page_range = [ p+1 for p in range(paginator.num_pages) ]
    return objects def get_mailman_client():
    # easier to patch during unit tests
    client = MailmanClient('%s/3.0' %
                settings.MAILMAN_REST_SERVER,
                settings.MAILMAN_API_USER,
                settings.MAILMAN_API_PASS)
    return client
from urllib2 import HTTPError
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.exceptions import SuspiciousOperation
from django.contrib.auth import login, get_backends
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import login as django_login_view
from django.shortcuts import render, redirect
from django.utils.http import is_safe_url
from django.utils.timezone import get_current_timezone
from django.http import Http404, HttpResponse
from social.backends.base import BaseAuth as SocialAuthBackend
from hyperkitty.models import (Favorite, LastView, MailingList, Sender,
    Email, Vote, Profile)
from hyperkitty.views.forms import (InternalAuthenticationForm,
    RegistrationForm, UserProfileForm)
from hyperkitty.lib.view_helpers import FLASH_MESSAGES, is_mlist_authorized
from hyperkitty.lib.paginator import paginate
from hyperkitty.lib.mailman import get_mailman_client
import dateutil.parser
import mailmanclient
import logging
    if "extra_context" not in kwargs:
        kwargs["extra_context"] = {}
    if "backends" not in kwargs["extra_context"]:
        kwargs["extra_context"]["social_backends"] = []
    # Note: sorry but I really find the .setdefault() method non-obvious and
    # harder to re-read that the lines above.
    for backend in get_backends():
        if not isinstance(backend, SocialAuthBackend):
            continue # It should be checked using duck-typing instead
        kwargs["extra_context"]["social_backends"].append(backend.name)
    kwargs["authentication_form"] = InternalAuthenticationForm
    return django_login_view(request, *args, **kwargs)


@login_required
def user_profile(request):
    if not request.user.is_authenticated():
        return redirect('hk_user_login')
    try:
        profile = Profile.objects.get(user=request.user)
    except Profile.DoesNotExist:
        # Create the profile if it does not exist. There's a signal receiver
        # that creates it for new users, but HyperKitty may be added to an
        # existing Django project with existing users.
        profile = Profile.objects.create(user=request.user)
    mm_user = profile.get_mailman_user()

    if request.method == 'POST':
        form = UserProfileForm(request.POST)
        if form.is_valid():
            request.user.first_name = form.cleaned_data["first_name"]
            request.user.last_name = form.cleaned_data["last_name"]
            profile.timezone = form.cleaned_data["timezone"]
            request.user.save()
            profile.save()
            # Now update the display name in Mailman
            if mm_user is not None:
                mm_user.display_name = "%s %s" % (
                        request.user.first_name, request.user.last_name)
                mm_user.save()
            redirect_url = reverse('hk_user_profile')
            redirect_url += "?msg=updated-ok"
            return redirect(redirect_url)
    else:
        form = UserProfileForm(initial={
                "first_name": request.user.first_name,
                "last_name": request.user.last_name,
                "timezone": get_current_timezone(),
                })

    # Favorites
    favorites = Favorite.objects.filter(user=request.user)

    # Emails
    other_addresses = profile.addresses[:]
    other_addresses.remove(request.user.email)

    # Flash messages
    flash_messages = []
    flash_msg = request.GET.get("msg")
    if flash_msg:
        flash_msg = { "type": FLASH_MESSAGES[flash_msg][0],
                      "msg": FLASH_MESSAGES[flash_msg][1] }
        flash_messages.append(flash_msg)

    # Extract the gravatar_url used by django_gravatar2.  The site
    # administrator could alternatively set this to http://cdn.libravatar.org/
    gravatar_url = getattr(settings, 'GRAVATAR_URL', 'http://www.gravatar.com')
    gravatar_shortname = '.'.join(gravatar_url.split('.')[-2:]).strip('/')

    context = {
        'user_profile' : profile,
        'form': form,
        'other_addresses': other_addresses,
        'favorites': favorites,
        'flash_messages': flash_messages,
        'gravatar_url': gravatar_url,
        'gravatar_shortname': gravatar_shortname,
    }
    return render(request, "hyperkitty/user_profile.html", context)


def user_registration(request):
    if not settings.USE_INTERNAL_AUTH:
        raise SuspiciousOperation
    try:
        redirect_to = request.GET["next"]
    except KeyError:
        try:
            redirect_to = request.POST["next"]
        except KeyError:
            redirect_to = reverse("hk_root")
    if not is_safe_url(url=redirect_to, host=request.get_host()):
        redirect_to = settings.LOGIN_REDIRECT_URL
    if request.user.is_authenticated():
        # Already registered, redirect back to index page
        return redirect(redirect_to)

    if request.POST:
        form = RegistrationForm(request.POST)
        if form.is_valid():
            user = form.save()
            user.backend = "django.contrib.auth.backends.ModelBackend"
            logger.info("New registered user: %s", user.username)
            if user.is_active:
                login(request, user)
                return redirect(redirect_to)
    else:
        form = RegistrationForm()

    context = {
        'form': form,
        'next': redirect_to,
    }
    return render(request, 'hyperkitty/register.html', context)


@login_required
def last_views(request):
    # Last viewed threads

    lviews = LastView.objects.filter(user=request.user