#!/usr/bin/python
# -*- coding: utf-8 -*-
"""Daily Report of Basecamp activity

Logs into a Basecamp site and posts a daily activity report from one 
(source) project to another (target) project. 

It is meant to run as a cronjob, but may be invoked manually from the 
command line, if needed, thus:

$ python basecamp_daily_report.py \\
  --hostname=hostname \\
  --username=username \\
  --password=password \\
  --source="Source Project" \\
  --target="Target Project" \\
  --category="Target Category" \\
  --subject="Daily Report"

Requires ElementTree [1], httplib2 [2] and Basecamp Wrapper [3].

[1] http://effbot.org/zone/element-index.htm
[2] httplib2: http://bitworking.org/projects/httplib2/
[3] Basecamp Wrapper: http://homework.nwsnet.de/products/79/
"""

__author__ = "Antonio Cavedoni <antonio@cavedoni.org>"
__svnid__ = "$Id$"

import sys
import time
import locale
import datetime
import elementtree.ElementTree as ET
import httplib2
import getopt
from basecamp import Basecamp

# Python 2.3 on Windows barks on locale names with underscores
# and Python 2.4.1 on OS X barks on them *without* underscores, oh well
if sys.platform == 'win32' and sys.version_info[0] >= 2 \
        and sys.version_info[1] <= 3:
    loc = 'it'
else:
    loc = 'it_IT'
# this stuff shouldn’t be here, this is a library, we’re not 
# supposed to mess with the environment locale settings
locale.setlocale(locale.LC_ALL, loc)

HOSTNAME = ''
USERNAME = ''
PASSWORD = ''
SOURCE_PROJECT = ''
TARGET_PROJECT = ''
TARGET_CATEGORY = ''
SUBJECT = ''

def iso2datetime(isotime):
    """Parses an ISO datetime like 2006-01-19T12:47:20Z
    and returns a datetime with no timezone 
    (Basecamp claims to return UTC dates)
    """
    return datetime.datetime(*time.strptime(isotime, '%Y-%m-%dT%H:%M:%SZ')[:6])

class Post:
    def __init__(self, xml_tree):
        self.id = int(xml_tree.find('id').text)
        self.title = xml_tree.find('title').text
        if xml_tree.find('body'): self.body = xml_tree.find('body').text
        self.posted_on = iso2datetime(xml_tree.find('posted-on').text)
        #if xml_tree.find('category'): 
        #    self.category_id = int(xml_tree.find('category.id').text)
        #    self.category_name = xml_tree.find('category.name').text
        self.attachments_count = int(xml_tree.find('attachments-count').text)

    def __repr__(self):
        return '<Post: %d from %s>' % \
            (self.id, self.posted_on.strftime('%Y-%m-%d'))

class TodoItem:
    def __init__(self, xml_tree):
        self.id = int(xml_tree.find('id').text)
        self.content = xml_tree.find('content').text
        self.created_on = iso2datetime(xml_tree.find('created-on').text)
        if xml_tree.find('completed').text == 'true':
            self.completed = True
            self.completed_on = iso2datetime(xml_tree.find('completed-on').text)
        else:
            self.completed = False
            self.completed_on = datetime.datetime(1950, 12, 12)


    def __repr__(self):
        return '<TodoItem: %d>' % self.id

def get_project_id(name):
    projects = ET.fromstring(bc.projects())
    for proj in projects.getiterator('project'):
        if proj.find('name').text == name:
            return int(proj.find('id').text)

def get_messages(proj):
    messages = ET.fromstring(bc.message_archive(proj))
    for m in messages.getiterator('post'):
        yield Post(m)

def get_todo_list_ids():
    todo_lists = ET.fromstring(bc.todo_lists(source_project_id))
    for todo_list in todo_lists.getiterator('todo-list'):
        yield int(todo_list.find('id').text)

def get_todo_items():
    for todo_list_id in get_todo_list_ids():
        todo_list = ET.fromstring(bc.todo_list(todo_list_id))
        for todo_item in todo_list.getiterator('todo-item'):
            yield TodoItem(todo_item)

def get_today_todo_items():
    for todo_item in get_todo_items():
        if ((todo_item.created_on - datetime.datetime.utcnow()) \
            > datetime.timedelta(days=-1)) or \
            ((todo_item.completed_on - datetime.datetime.utcnow()) \
             > datetime.timedelta(days=-1)):
            yield todo_item   

def get_today_messages():
    for message in get_messages(get_project_id(SOURCE_PROJECT)):
        # if posted in the last week
        if (message.posted_on - datetime.datetime.utcnow()) \
            > datetime.timedelta(days=-1):
            yield message

def get_target_category_id(name):
    categories = ET.fromstring(bc.message_categories(get_project_id(TARGET_PROJECT)))
    for cat in categories.getiterator('post-category'):
        if cat.find('name').text == name:
            return int(cat.find('id').text)

def posts_today():
    post = []
    messages = []
    for m in get_today_messages():
        messages.append(m)
    if len(messages) != 0:
        post.append("I messaggi delle ultime 24 ore:\n")
        for m in messages:
            post.append("* %s" % m.title.encode('utf-8'))
    todo_items = []
    for i in get_today_todo_items():
        todo_items.append(i)

    if (len(todo_items) + len(messages)) == 0:
        print "There was no activity in the past 24 hours, abort."
        sys.exit(0)

    post.append("")
    if len(todo_items) != 0:
        post.append("I todo-items delle ultime 24 ore:\n")
        for item in todo_items:
            if item.completed:
                # @@HACK! We’re doing decode('latin1') because we 
                # know that the locale will be 'it' or 'it_IT', 
                # but that is hardcoded and… well, just wrong
                post.append("* -%s- _(completato %s)_" % (
                item.content.encode('utf-8'),
                item.completed_on.strftime(
                    '%A %d/%m/%Y').decode('latin1').lower().encode('utf-8')
                ))
            else:
                post.append("* %s _(creato %s)_" % (
                item.content.encode('utf-8'),
                item.created_on.strftime(
                    '%A %d/%m/%Y').decode('latin1').lower().encode('utf-8')
                ))
    return '\n'.join(post)

def create_message(proj_id, category_id, subject, message):
    """Brute-force attempt to create a new Basecamp post, since the 
    method in the Python Basecamp library is not working properly
    """
    POST_CREATE_URI = "http://%s/projects/%s/msg/create" % \
        (HOSTNAME, proj_id)
    h = httplib2.Http()
    h.add_credentials(USERNAME, PASSWORD)
    request_body = """
<request>
  <post>
    <category-id>%s</category-id>
    <title>%s</title>
    <body>%s</body>
    <extended-body/>
    <use-textile>1</use-textile> 
    <private>0</private>
  </post>
</request>
""" % (category_id, subject, message)
    response, content = h.request(POST_CREATE_URI, 'POST', body=request_body, 
              headers={'Accept': 'application/xml', 
                       'Content-type': 'application/xml'}
              )

def usage():
    print __doc__

if __name__ == "__main__":
    try:
        opts, args = getopt.getopt(
            sys.argv[1:], 
            "hupstcj", 
            ("hostname=", "username=", "password=", "source=", "target=",
             "category=", "subject=")
            )
    except getopt.GetoptError:
        usage()
        sys.exit(2)
    if not opts:
        usage()
    else:
        for o, a in opts:
            if o in "--hostname":
                HOSTNAME = a
            elif o in "--username":
                USERNAME = a
            elif o in "--password":
                PASSWORD = a
            elif o in "--source":
                SOURCE_PROJECT = a
            elif o in "--target":
                TARGET_PROJECT = a
            elif o in "--category":
                TARGET_CATEGORY = a
            elif o in "--subject":
                SUBJECT = a
        # @@refactor this! all these globals make my head hurt
        bc = Basecamp("http://%s/" % HOSTNAME, USERNAME, PASSWORD)
        source_project_id = get_project_id(SOURCE_PROJECT)
        create_message(get_project_id(TARGET_PROJECT), 
                       get_target_category_id(TARGET_CATEGORY), 
                       SUBJECT, 
                       posts_today())


