Commits

Tetsuya Morimoto committed 9c8d3f7

initial commit

Comments (0)

Files changed (11)

+.installed.cfg
+.pyc
+.swp
+build
+develop-eggs
+dist
+eggs
+parts
+.*.egg-info
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+include buildout.cfg
+include bootstrap.py
+include MANIFEST.in
+include LICENSE
+recursive-include src *.py *.txt *.html *.css *.js *.png
+Notes
+=====
+
+`TracTicketReferencePlugin`_ adds simple ticket cross-reference for Trac.
+
+Note: TracTicketReference requires Trac 0.12 or higher.
+
+.. _TracTicketReferencePlugin: http://trac-hacks.org/wiki/TracTicketReferencePlugin
+
+What is it?
+-----------
+
+This plugin adds "Relationships" fields to each ticket, enabling you
+to express cross-reference between tickets. 
+
+Configuration
+=============
+
+To enable the plugin::
+
+    [components]
+    ticketref.* = enabled
+
+    [ticket-custom]
+    ticketref = text
+    ticketref.label = Relationships
+
+Custom fields
+-------------
+While the field names must be ``ticketref``, you are free to use any
+text for the field labels.
+
+Acknowledgment
+==============
+
+This plugin was inspired by `MasterTicketsPlugin`_.
+
+.. _MasterTicketsPlugin: http://trac-hacks.org/wiki/MasterTicketsPlugin
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from os.path import join as pathjoin
+from setuptools import setup, find_packages
+
+VERSION = "0.1.0"
+LONG_DESCRIPTION = "".join(
+    open("README.txt").read(),
+)
+
+REQUIRES = [
+    "Trac >= 0.12",
+]
+
+CLASSIFIERS = [
+    "Framework :: Trac",
+    "Development Status :: 4 - Beta",
+    "Environment :: Web Environment",
+    "License :: OSI Approved :: Apache Software License",
+    "Intended Audience :: Developers",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python",
+    "Topic :: Software Development",
+]
+
+setup(
+    name="TracTicketReferencePlugin",
+    version=VERSION,
+    description="Provides support for ticket cross reference for Trac",
+    long_description=LONG_DESCRIPTION,
+    classifiers=CLASSIFIERS,
+    keywords=["trac", "plugin", "ticket", "cross-reference"],
+    author="Tetsuya Morimoto",
+    author_email="tetsuya dot morimoto at gmail dot com",
+    url="http://trac-hacks.org/wiki/TracTicketReferencePlugin",
+    license="Apache License 2.0",
+    packages=["ticketref"],
+    package_data={
+        "ticketref": ["templates/*.html", "htdocs/*.js", "htdocs/*.css",
+                      "templates/*.png"],
+    },
+    include_package_data=True,
+    install_requires=REQUIRES,
+    entry_points = {
+        "trac.plugins": [
+            "ticketref.web_ui = ticketref.web_ui",
+            "ticketref.api = ticketref.api",
+        ]
+    }
+)

ticketref/__init__.py

+__import__('pkg_resources').declare_namespace(__name__)
+# -*- coding: utf-8 -*-
+
+from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
+from trac.ticket.api import ITicketChangeListener, ITicketManipulator
+from trac.ticket.model import Ticket
+
+from model import CUSTOM_FIELDS, TICKETREF, TicketLinks
+
+
+class TicketRefsPlugin(Component):
+    """ Extend custom field for ticket cross-reference """
+
+    implements(IEnvironmentSetupParticipant,
+               ITicketChangeListener, ITicketManipulator)
+
+    # IEnvironmentSetupParticipant methods
+    def environment_created(self):
+        self.upgrade_environment(self.env.get_db_cnx())
+
+    def environment_needs_upgrade(self, db):
+        for field in CUSTOM_FIELDS:
+            if field["name"] not in self.config["ticket-custom"]:
+                return True
+        return False
+
+    def upgrade_environment(self, db):
+        custom = self.config["ticket-custom"]
+        for field in CUSTOM_FIELDS:
+            if field["name"] not in custom:
+                custom.set(field["name"], field["type"])
+                for key, value in field["properties"]:
+                    custom.set(key, value)
+                self.config.save()
+
+    def has_ticket_refs(self, ticket):
+        refs = ticket[TICKETREF]
+        return refs and refs.strip()
+
+    # ITicketChangeListener methods
+    def ticket_created(self, ticket):
+        if self.has_ticket_refs(ticket):
+            self.log.debug("TracTicketReference: ticket are creating")
+            links = TicketLinks(self.env, ticket)
+            links.create()
+
+    def ticket_changed(self, ticket, comment, author, old_values):
+        if TICKETREF in old_values:
+            self.log.debug("TracTicketReference: ticket are changing")
+            links = TicketLinks(self.env, ticket)
+            links.change(author, old_values[TICKETREF])
+
+    def ticket_deleted(self, ticket):
+        if self.has_ticket_refs(ticket):
+            self.log.debug("TracTicketReference: ticket are deleting")
+            links = TicketLinks(self.env, ticket)
+            links.delete()
+
+    # ITicketManipulator methods
+    def prepare_ticket(self, req, ticket, fields, actions):
+        pass
+
+    def validate_ticket(self, req, ticket):
+        if self.has_ticket_refs(ticket):
+            for _id in ticket[TICKETREF].split(","):
+                ref_id = int(_id.strip())
+                try:
+                    assert ref_id != ticket.id
+                    Ticket(self.env, ref_id)
+                except Exception, err:
+                    _prop = ("ticket-custom", "ticketref.label")
+                    yield self.env.config.get(*_prop), err

ticketref/model.py

+# -*- coding: utf-8 -*-
+
+from datetime import datetime
+
+from trac.ticket.model import Ticket
+from trac.util.datefmt import utc, to_utimestamp
+
+from utils import cnv_text2list, cnv_list2text
+
+CUSTOM_FIELDS = [
+    {"name": "ticketref",
+     "type": "text",
+     "properties": [("ticketref.label", "Relationships")],
+    },
+]
+TICKETREF = CUSTOM_FIELDS[0]["name"]
+
+SELECT_TICKETREF = "SELECT value FROM ticket_custom "\
+                   "WHERE ticket=%%s AND name='%s'" % TICKETREF
+INSERT_TICKETREF = "INSERT INTO ticket_custom (ticket, name, value) "\
+                   "VALUES (%%s, '%s', '%%s')" % TICKETREF
+UPDATE_TICKETREF = "UPDATE ticket_custom SET value='%%s' "\
+                   "WHERE ticket=%%s AND name='%s'" % TICKETREF
+DELETE_TICKETREF = "DELETE FROM ticket_custom "\
+                   "WHERE ticket=%%s AND name='%s'" % TICKETREF
+INSERT_TICKETCHG = "INSERT INTO ticket_change "\
+                   "(ticket, time, author, field, oldvalue, newvalue) "\
+                   "VALUES (%%s, %%s, '%%s', '%s', '%%s', '%%s')" % TICKETREF
+
+
+class TicketLinks(object):
+    """A model for the ticket links as cross reference."""
+
+    def __init__(self, env, ticket):
+        self.env = env
+        self.db = env.get_db_cnx()
+        self.cursor = self.db.cursor()
+        if not isinstance(ticket, Ticket):
+            ticket = Ticket(self.env, ticket)
+        self.ticket = ticket
+        self.time_stamp =  to_utimestamp(datetime.now(utc))
+
+    def remove_cross_reference(self, refs, author):
+        c = self.cursor
+        for ref_id in refs:
+            c.execute(SELECT_TICKETREF % ref_id)
+            row = (c.fetchone() or ("",))[0]
+            target_refs = cnv_text2list(row)
+            target_refs.remove(self.ticket.id)
+            if target_refs:
+                new_text = cnv_list2text(target_refs)
+                c.execute(UPDATE_TICKETREF % (new_text, ref_id))
+            else:
+                c.execute(DELETE_TICKETREF % ref_id)
+            c.execute(INSERT_TICKETCHG % (
+                ref_id, self.time_stamp, author, self.ticket.id, ""))
+
+    def add_cross_reference(self, refs, author):
+        c = self.cursor
+        for ref_id in refs:
+            c.execute(SELECT_TICKETREF % ref_id)
+            row = (c.fetchone() or ("",))[0]
+            target_refs = cnv_text2list(row)
+            target_refs.add(self.ticket.id)
+            new_text = cnv_list2text(target_refs)
+            if row:
+                c.execute(UPDATE_TICKETREF % (new_text, ref_id))
+            else:
+                c.execute(INSERT_TICKETREF % (ref_id, self.ticket.id))
+            c.execute(INSERT_TICKETCHG % (
+                ref_id, self.time_stamp, author, "", self.ticket.id))
+
+    def create(self):
+        refs = cnv_text2list(self.ticket[TICKETREF])
+        self.add_cross_reference(refs, self.ticket["reporter"])
+        self.db.commit()
+        self.env.log.debug("TracTicketReference: cross-refs are created")
+
+    def change(self, author, old_refs_text):
+        old_refs = cnv_text2list(old_refs_text)
+        new_refs = cnv_text2list(self.ticket[TICKETREF])
+        self.remove_cross_reference(old_refs - new_refs, author)
+        self.add_cross_reference(new_refs - old_refs, author)
+        self.db.commit()
+        self.env.log.debug("TracTicketReference: cross-refs are updated")
+
+    def delete(self):
+        refs = cnv_text2list(self.ticket[TICKETREF])
+        self.remove_cross_reference(refs, "admin")
+        self.db.commit()
+        self.env.log.debug("TracTicketReference: cross-refs are deleted")

ticketref/templates/dummy.html

Empty file added.

ticketref/utils.py

+# -*- coding: utf-8 -*-
+
+def cnv_text2list(refs_text):
+    """ convert text to list
+    >>> cnv_text2list("")
+    set([])
+    >>> cnv_text2list("1")
+    set([1])
+    >>> cnv_text2list("1, 3")
+    set([1, 3])
+    >>> cnv_text2list("  1,3,   5")
+    set([1, 3, 5])
+    """
+    refs = set([])
+    if refs_text and refs_text.strip():
+        refs = set([int(id_.strip()) for id_ in refs_text.split(",")])
+    return refs
+
+def cnv_list2text(refs):
+    """ convert list to text
+    >>> cnv_list2text(set([]))
+    u''
+    >>> cnv_list2text(set([1]))
+    u'1'
+    >>> cnv_list2text(set([3, 1]))
+    u'1, 3'
+    """
+    return u", ".join(str(i) for i in sorted(refs))

ticketref/web_ui.py

+# -*- coding: utf-8 -*-
+
+from pkg_resources import resource_filename
+
+from genshi.builder import tag
+from trac.core import *
+from trac.web.api import ITemplateStreamFilter
+from trac.web.chrome import ITemplateProvider
+from trac.resource import ResourceNotFound
+from trac.ticket.model import Ticket
+from trac.util.text import shorten_line
+
+from model import TICKETREF as TREF
+from utils import cnv_text2list
+
+TEMPLATE_FILES = [
+    "report_view.html", "query_results.html", "ticket.html", "query.html",
+]
+
+
+class TicketRefsTemplate(Component):
+    """ Extend template for ticket cross-reference """
+
+    implements(ITemplateStreamFilter, ITemplateProvider)
+
+    # ITemplateStreamFilter methods
+    def filter_stream(self, req, method, filename, stream, data):
+        if not data or (not filename in TEMPLATE_FILES):
+            return stream
+
+        # For ticket.html
+        if "fields" in data and isinstance(data["fields"], list):
+            self._filter_fields(req, data)
+
+        # For query_results.html and query.html
+        if "groups" in data and isinstance(data["groups"], list):
+            self._filter_groups(req, data)
+
+        # For report_view.html
+        if "row_groups" in data and isinstance(data["row_groups"], list):
+            self._filter_row_groups(req, data)
+
+        return stream
+
+    def _filter_fields(self, req, data):
+        for field in data["fields"]:
+            if field["name"] == TREF and data["ticket"][TREF]:
+                field["rendered"] = self._link_refs(req, data["ticket"][TREF])
+
+    def _filter_groups(self, req, data):
+        for group, tickets in data["groups"]:
+            for ticket in tickets:
+                if TREF in ticket:
+                    ticket[TREF] = self._link_refs(req, ticket[TREF])
+
+    def _filter_row_groups(self, req, data):
+        for group, rows in data["row_groups"]:
+            for row in rows:
+                _is_list = isinstance(row["cell_groups"], list)
+                if "cell_groups" in row and _is_list:
+                    for cells in row["cell_groups"]:
+                        for cell in cells:
+                            if cell.get("header", {}).get("col") == TREF:
+                                cell["value"] = self._link_refs(req,
+                                                                cell["value"])
+
+    def _link_refs(self, req, refs_text):
+        items = []
+        for ref_id in cnv_text2list(refs_text):
+            elem = "#%s" % ref_id
+            try:
+                ticket = Ticket(self.env, ref_id)
+                if "TICKET_VIEW" in req.perm(ticket.resource):
+                    elem = tag.a("#%s" % ref_id, class_=ticket["status"],
+                                 href=req.href.ticket(ref_id),
+                                 title=shorten_line(ticket["summary"]))
+            except ResourceNotFound:
+                pass  # not supposed to happen, just in case
+            items.append(elem)
+            items.append(", ")
+        return items and tag(items[:-1]) or None
+
+    # ITemplateProvider methods
+    def get_htdocs_dirs(self):
+        """Return the absolute path of a directory containing additional
+        static resources (such as images, style sheets, etc).
+        """
+        return [("ticketref", resource_filename(__name__, "htdocs"))]
+
+    def get_templates_dirs(self):
+        """Return the absolute path of the directory containing the provided
+        ClearSilver templates.
+        """
+        return [resource_filename(__name__, "templates")]