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 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 ThreadCategory(models.Model):
    name = models.CharField(max_length=255, db_index=True, unique=True)
    color = ColorPickerField()

    class Meta:
        verbose_name_plural = "Thread categories"

    def __unicode__(self):
        return u'Thread category "%s"' % (unicode(self.name)) 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 get_category_widget(request=None, current_category=None, categories=()):
    """
    Returns the category form and the applicable category object (or None if no
    category is set for this thread).

    If current_category is not provided or None, try to deduce it from the POST
    request.
    if request is not provided or None, don't return the category form, return
    None instead.
    """

    if request:
        if request.method == "POST":
            category_form = CategoryForm(request.POST)
        else:
            category_form = CategoryForm(initial={"category": current_category or ""})
        category_form["category"].field.choices = categories
    else:
        category_form = None
    if request and request.method == "POST" and category_form.is_valid():
        # is_valid() must be called after the choices have been set
        current_category = category_form.cleaned_data["category"]

    if not current_category:
        category = None
    else:
        try:
            category = ThreadCategory.objects.get(name=current_category)
        except ThreadCategory.DoesNotExist:
            category = None
    return category, category_form def get_months(mlist):
    """ Return a dictionnary of years, months for which there are
    potentially archives available for a given list (based on the
    oldest post on the list).

    :arg list_name, name of the mailing list in which this email
    should be searched.
    """
    date_first = cache.get_or_set(
        "MailingList:%s:first_date" % mlist.name,
        lambda: mlist.emails.order_by("date"
            ).values_list("date", flat=True).first(),
        None)
    if not date_first:
        return {}
    archives = {}
    now = datetime.datetime.now()
    year = date_first.year
    month = date_first.month
    while year < now.year:
        archives[year] = range(1, 13)[(month -1):]
        year = year + 1
        month = 1
    archives[now.year] = range(1, 13)[:now.month]
    return archives def get_display_dates(year, month, day):
    if day is None:
        start_day = 1
    else:
        start_day = int(day)
    begin_date = datetime.datetime(
        int(year), int(month), start_day, tzinfo=utc)

    if day is None:
        end_date = begin_date + datetime.timedelta(days=32)
        end_date = end_date.replace(day=1)
    else:
        end_date = begin_date + datetime.timedelta(days=1)

    return begin_date, end_date def daterange(start_date, end_date):
    for n in range(int((end_date - start_date).days)):
        yield start_date + datetime.timedelta(n) def check_mlist_private(func):
    @wraps(func, assigned=available_attrs(func))
    def inner(request, *args, **kwargs):
        if "mlist_fqdn" in kwargs:
            mlist_fqdn = kwargs["mlist_fqdn"]
        else:
            mlist_fqdn = args[0]
        try:
            mlist = MailingList.objects.get(name=mlist_fqdn)
        except MailingList.DoesNotExist:
            raise Http404("No archived mailing-list by that name.")
        if not is_mlist_authorized(request, mlist):
            return render(request, "hyperkitty/errors/private.html", {
                            "mlist": mlist,
                          }, status=403)
        return func(request, *args, **kwargs)
    return inner 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
import datetime
import json
from django.shortcuts import redirect, render, get_object_or_404
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils import formats
from django.utils.dateformat import format as date_format
from django.http import Http404, HttpResponse
from hyperkitty.models import Favorite, MailingList, ThreadCategory
from hyperkitty.lib.view_helpers import (FLASH_MESSAGES, get_category_widget,
    get_months, get_display_dates, daterange, check_mlist_private)
from hyperkitty.lib.paginator import paginate
    from hyperkitty.lib.mockup import generate_top_author
        "no_results_text": no_results_text,
    }
    if day is None:
        extra_context["participants_count"] = \
            mlist.get_participants_count_for_month(int(year), int(month))
    return _thread_list(request, mlist, threads, extra_context=extra_context)


def _thread_list(request, mlist, threads, template_name='hyperkitty/thread_list.html', extra_context=None):
    categories = [ (c.name, c.name.upper())
                   for c in ThreadCategory.objects.all() ] \
                 + [("", "no category")]
    threads = paginate(threads, request.GET.get('page'))
    for thread in threads:
        # Favorites
        thread.favorite = False
        if request.user.is_authenticated():
            try:
                Favorite.objects.get(thread=thread, user=request.user)
            except Favorite.DoesNotExist:
                pass
            else:
                thread.favorite = True
        # Category
        thread.category_hk, thread.category_form = \
            get_category_widget(request, thread.category, categories)

    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)

    context = {
        'mlist' : mlist,
        'threads': threads,
        'months_list': get_months(mlist),
        'flash_messages': flash_messages,
    }
    if extra_context is not None:
        context.update(extra_context)
    return render(request, template_name, context)


@check_mlist_private
def overview(request, mlist_fqdn=None):
    if not mlist_fqdn:
        return redirect('/')
    mlist = get_object_or_404(MailingList, name=mlist_fqdn)
    threads = []
    for thread_obj in mlist.recent_threads:
        thread_obj.category_widget = get_category_widget(
                None, thread_obj.category)[0]
        threads.append(thread_obj)

    # top threads are the one with the most answers
    top_threads = sorted(threads, key=lambda t: t.emails_count, reverse=True)

    # active threads are the ones that have the most recent posting
    active_threads = sorted(threads, key=lambda t: t.date_active, reverse=True)

    # top authors are the ones that have the most kudos.  How do we determine
    # that?  Most likes for their post?
    if settings.USE_MOCKUPS:
        authors = generate_top_author()
        authors = sorted(authors, key=lambda author: author.kudos)
        authors.reverse()
    else:
        authors = []

    # Popular threads
    pop_threads = []
    for t in threads:
        votes = t.get_votes()
        if votes["likes"] - votes["dislikes"] > 0:
            pop_threads.append(t)
    def _get_thread_vote_result(t):
        votes = t.get_votes()
        return votes["likes"] - votes["dislikes"]
    pop_threads.sort(key=_get_thread_vote_result, reverse=True)

    # Threads by category
    threads_by_category = {}
    for thread in active_threads:
        if not thread.category:
            continue
        # don't use defaultdict, use .setdefault():
        # http://stackoverflow.com/questions/4764110/django-template-cant-loop-defaultdict
        if len(threads_by_category.setdefault(thread.category, [])) >= 5:
            continue
        threads_by_category[thread.category].append(thread)

    # Personalized discussion groups: flagged/favorited threads and threads by user
    if request.user.is_authenticated():
        favorites = [ f.thread for f in Favorite.objects.filter(
            thread__mailinglist=mlist, user=request.user) ]
        mm_user_id = request.user.hyperkitty_profile.get_mailman_user_id()
        threads_posted_to = mlist.threads.filter(
            emails__sender__mailman_id=mm_user_id).distinct()
    else:
        favorites = []
        threads_posted_to = []

    # Empty messages # TODO: translate this
    empty_messages = {
        "flagged": 'You have not flagged any discussions (yet).',
        "posted": 'You have not posted to this list (yet).',
        "active": 'No discussions this month (yet).',
        "popular": 'No vote has been cast this month (yet).',
    }

    context = {
        'view_name': 'overview',
        'mlist' : mlist,
        'top_threads': top_threads[:20],
        'most_active_threads': active_threads[:20],
        'top_author': authors,
        'pop_threads': pop_threads[:20],
        'threads_by_category': threads_by_category,

        'months_list': get_months(mlist),