[Tracker-discuss] List of recently modified issues

Michał Kwiatkowski constant.beta at gmail.com
Sun Jun 10 22:22:16 CEST 2007


On 6/6/07, "Martin v. Löwis" <martin at v.loewis.de> wrote:
> I'd encourage you to check out the tracker installation, and
> propose a solution. There are two ways for report generation
> in roundup: you can either update it whenever a change is made.
> This is efficient, but also fixed with respect to the
> information available in the report. OTOH, generating the report
> dynamically allows customization, but is also more
> compute-expensive.

I went the update-on-change road, as it seemed more straightforward.
Attached to this mail is "changes_xml_writer.py" detector, which
updates recent-changes.xml file each time a file is uploaded to a
tracker. After uploading a sample attachment go to
http://localhost:9999/python-dev/_file/recent-changes.xml to see the
generated file. A single change looks like this in XML:

<change date="Sun, 10 Jun 2007 20:00:58 +0000"
id="file45-added-to-issue6" type="file-added">
  <file-id>45</file-id>
  <file-name>sample.patch</file-name>
  <file-type>text/x-diff</file-type>
  <file-url>http://localhost:9999/python-dev/file45/sample.patch</file-url>
  <issue-id>6</issue-id>
</change>

Currently only file uploads are reported, although more change types
can be implemented later. File uploads are everything what I need to
continue my SoC project.

List of changes has maximum of 20 items, so the file won't grow indefinitely.

I have a question about Roundup updates though. Is a reactor function
called atomically, i.e. should I worry about two processes writing to
recent-changes.xml?

Cheers,
mk
-------------- next part --------------
import os
import urllib
from xml.dom import minidom
from time import gmtime, strftime

# Relative to tracker home directory.
FILENAME = os.path.join('%(TEMPLATES)s', 'recent-changes.xml')


def tracker_url(db):
    return str(db.config.options[('tracker', 'web')])

def changes_xml_path(db):
    return os.path.join(db.config.HOME, FILENAME % db.config.options)

def rfc2822_date():
    return strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())

class File(object):
    def __init__(self, db, id, issue_id):
        self.db = db
        self.id = id
        self.issue_id = issue_id

        self.name = db.file.get(id, 'name')
        self.type = db.file.get(id, 'type')
        # Based on roundup.cgi.templating._HTMLItem.download_url().
        self.download_url = tracker_url(self.db) +\
            urllib.quote('%s%s/%s' % ('file', self.id, self.name))

class ChangesXml(object):
    # Maximum number of changes stored in a file.
    max_items = 20

    def __init__(self, filename):
        self.filename = filename
        self._read_document()

    def save(self):
        self._trim_to_max_items()

        fd = open(self.filename, 'w')
        self.document.writexml(fd, encoding="UTF-8")
        fd.close()

    def add_file(self, file):
        change = self._change("file%s-added-to-issue%s" % (file.id, file.issue_id),
                              "file-added")

        change.appendChild(self._element_with_text("file-id",   file.id))
        change.appendChild(self._element_with_text("file-name", file.name))
        change.appendChild(self._element_with_text("file-type", file.type))
        change.appendChild(self._element_with_text("file-url",  file.download_url))
        change.appendChild(self._element_with_text("issue-id",  file.issue_id))

        self.root.appendChild(change)

    def add_files(self, files):
        for file in files:
            self.add_file(file)

    def _change(self, id, type):
        """Return new 'change' element of a given type.
           <change id='id' date='now' type='type'></change>
        """
        change = self.document.createElement("change")
        change.setAttribute("id",   id)
        change.setAttribute("type", type)
        change.setAttribute("date", rfc2822_date())
        return change

    def _element_with_text(self, name, value):
        """Return new element with given name and text node as a value.
           <name>value</name>
        """
        element = self.document.createElement(name)
        text = self.document.createTextNode(str(value))
        element.appendChild(text)
        return element

    def _trim_to_max_items(self):
        """Remove changes exceeding self.max_items.
        """
        # Assumes that changes are stored sequentially from oldest to newest.
        # Will do for now.
        for change in self.root.getElementsByTagName("change")[0:-self.max_items]:
            self.root.removeChild(change)

    def _read_document(self):
        try:
            self.document = minidom.parse(self.filename)
            self.root = self.document.firstChild
        except IOError, e:
            if e.errno != 2:
                raise
            self._create_new_document()

    def _create_new_document(self):
        self.document = minidom.Document()
        self.root = self.document.createElement("changes")
        self.document.appendChild(self.root)

def get_new_files_ids(issue_now, issue_then):
    """Return ids of files added between `now` and `then`.
    """
    files_now = set(issue_now['files'])
    files_then = set(issue_then['files']) if issue_then else set()
    return map(int, files_now - files_then)

def file_added_to_issue(db, cl, issue_id, olddata):
    changes   = ChangesXml(changes_xml_path(db))
    issue     = db.issue.getnode(issue_id)
    new_files = [ File(db, id, issue_id) for id in get_new_files_ids(issue, olddata) ]

    changes.add_files(new_files)
    changes.save()


def init(db):
    db.issue.react('create', file_added_to_issue)
    db.issue.react('set',    file_added_to_issue)


More information about the Tracker-discuss mailing list