Compare commits

...

96 Commits
3.2.0 ... main

Author SHA1 Message Date
1064de3e1b Add support for syncing the parent property (if it exists). 2025-01-13 08:16:07 -06:00
8b0c751344 Bump version for 4.29.0 release. 2025-01-11 11:59:52 -06:00
0f7e257f76 Cap output width to 80 characters max. 2025-01-11 11:59:17 -06:00
76225d1c50 Add the autoSync config property to automatically sync contexts after relevant issues change. 2025-01-11 11:58:47 -06:00
0361d1b869 Add the --silent option to allow suppressing log output. 2025-01-11 11:58:02 -06:00
40cb602362 Add support for syncing to Probatem's Virtual Status Board. 2025-01-09 11:57:32 -06:00
e2a306c1d6 Refactor to clean up package source structure. 2025-01-08 18:07:19 -06:00
e955cd5b24 Add list tags sub-command, accept properties update to the edit command. 2025-01-06 10:38:47 -06:00
4176dfea3a Migrate from asdf to mise. 2024-12-10 10:00:53 -06:00
587e3c4509 Update for Nim 2.x. No longer building pit_api.
`pit_api` cannot be built for Nim 2.x without a non-trivial refactor due
to the enforcement of gc-safety. See the `pit_api.nim` source for
details.
2024-11-30 08:08:40 -06:00
f6a97c384a Use update_nim_package_version from the package repository. 2024-11-30 07:56:55 -06:00
0c3d73dc2b Add debug logging when changing the state of an issue. 2024-08-12 10:52:51 -05:00
9a0bf35882 Add asdf tool-versions definitions pinning this to Nim 1.6 2024-05-10 11:58:35 -05:00
be7c099b7b Format listed issues plainly when STDOUT is not a TTY 2024-01-01 12:55:01 -06:00
d04797460c Format listed issues plainly when STDIN is not a TTY
When calling pit from other programs or as part of a pipe, the display
style typically used to format listed issues contains a lot of unwanted
output (ANSI escape code, headings, etc.). Now when STDIN is not a TTY,
a plain and consistently formatted version of the issues is listed
without any additional formatting, one issue per line of output.
2024-01-01 12:47:43 -06:00
8cf0bf5d98 Change how time entries based on PIT issues are logged when using --ptk. 2023-12-17 07:32:46 -06:00
ddad90ddef Add examples to the online help for all special properties. 2023-12-15 21:34:38 -06:00
34ce2b61b9 When creating new recurrences, put them in the TodoToday state, not Todo. 2023-07-06 08:07:22 -05:00
661d5959c6 Add show-dupes command, fix BareExcept warnings. 2023-05-19 09:24:53 -05:00
6665f09b7b Fixed missed version bump in cliconstants. 2023-05-19 09:05:02 -05:00
bcb1c7c17c Extract logic for locating the config file to the cliutils library. 2023-05-13 07:30:25 -05:00
b0e3f5a9d8 Expose issue formating functionality. 2023-03-21 11:11:44 -05:00
fee4ba70a6 Update state field when changing an issue's state. 2023-03-21 10:27:25 -05:00
171adbb59d Make IssueState available as a field on Issue.
* Add `state` on `Issue` to be able to query the state of an issue even
  if you only have a reference to this issue and don't have a reference
  to the context or issues table. This does not change the persisted
  format of the issue. On disk the state of an issue is still
  represented by it's location in the file hierarchy.

* Refactored libpit to use zero_functional instead of sequtils.
2023-03-21 08:30:29 -05:00
d01d6e37f4 Update timeutils version to include support for the shorter ISO8601 date format. 2023-02-28 23:29:06 -06:00
b98596574d Add find utility method for searching for issues among multiple issue states. 2023-02-17 12:12:13 -06:00
ea9f8ea7ac Move issue loading logic into the publicly-exposed library methods. 2023-02-16 11:07:09 -06:00
ae4a943e82 Allow access to pit functionality as a Nim libaray. 2023-02-16 09:07:02 -06:00
58a5321d95 Rework dependencies using JDB Softwar package repo instead of URLs. 2023-02-13 08:48:40 -06:00
7215b4969b Re-design output to make skimming easier.
- We now always protect the left margin when printing task details
  (including tags) to make it easier to skim down that line.
- Also made the actual summary always follow immediately after the ID,
  to align to that skimmable line.
- Moved the information about the delegatee to the end of the summary,
  next to the tags, and changed the color of the delegatee to make it
  easier to distinguish.
- Added the `-G` option, to allow filtering out issues matching any of
  the provided tags.
- We now allow options to be passed to both the `delegate` and `help`
  command. Any options are ignored, but this allows the use of tools
  like `cmd_shell` which always wrap commands with the pre-given
  options.
2022-07-31 20:01:39 -05:00
c7891de310 Show Pending in either Today's list of the Future list, but not both at the same time. 2022-07-28 10:48:51 -05:00
a373af0658 Add support for filtering based on property exclusion.
For example, allow commands like:

  # exclude issues from context "abc"
  pit list -C abc

  # exclude issues delegated to John Doe
  pit list -P "delgated-to:John Doe"
2022-07-22 10:39:13 -05:00
de3ee05680 Change logging format. 2022-01-22 10:31:58 -06:00
59440d2c9d Remove unused, old copies of usage and outline text. 2022-01-21 15:01:07 -06:00
6226ff21c5 Fix compile-time message includes when installing via Nimble. 2022-01-21 14:59:22 -06:00
71e035fdbe Add --show-hidden to ignore hide-until properties. 2022-01-04 22:27:08 -06:00
df854f864c When completing an issue, print info about any new issue created by recurrence. 2021-09-21 10:14:49 -05:00
7bccd83e23 Add support for issue recurrence. 2021-09-17 13:51:37 -05:00
b25d2be164 Adding more comrehensive documentaition regaring issue types and properties. 2021-09-16 23:31:12 -05:00
e0ab3cb401 Include Pending in the 'today' view. Allow listing multiple issues or states. 2021-08-30 11:49:35 -05:00
d93c0cf348 Add the ability to hide tasks until a certain date. 2021-08-24 10:56:47 -05:00
9606e71cec Add support for binary properties via data URIs. 2021-06-07 18:34:41 -05:00
98f4dda1ad Reset terminal attributes after printing single issues. 2021-02-01 07:41:01 -06:00
393be347c9 Default list command no longer includes Done issues. 2020-11-12 14:50:43 -06:00
f8fed9d937 Allow filtering by issue tags. 2020-11-12 07:37:22 -06:00
ef16eafd48 Update to address changes for Nim 1.x (currently 1.4.0). 2020-11-12 04:05:04 -06:00
4af0d09356 Remove stuff in the README that I don't understand. 🤷 2020-10-02 12:33:51 -05:00
071c4b66e5 Explicitly document list contexts subcommand. Sort listed contexts alphabetically. 2020-10-02 12:13:22 -05:00
57a3af4f2f When moving issues, create target directories if they do not already exist. 2020-07-06 11:47:31 -05:00
08b9df2086 Update to reflect changes in Nim stdlib in v1.2. 2020-07-06 11:39:57 -05:00
339e88cddd list contexts command now prints both the listeral context value and the display name. 2020-05-05 09:40:08 -05:00
0a2249018b Add ptk integration option as a command-line switch. 2020-04-16 10:22:56 -05:00
ec3008937d Add README, bugfix CLI configuration. 2020-03-23 09:18:44 -05:00
10fcc34ea2 Update nim-cli-util dependency version to get a fix to the queryParamsToCli function. 2020-03-23 08:27:30 -05:00
4127fbe41c Better PTK integration.
- Now includes the context as a PTK tag (if present).
- Add the PIT ID to the PTK notes.
2020-03-16 09:39:17 -05:00
0671d7728e Add helper to update version easily. 2020-02-16 00:57:58 -06:00
7b5f26f24a Update dependency references to use full URLs to non-central libs. 2020-02-14 11:21:54 -06:00
Jonathan Bernard
db3e648d47 Add tag and untag commands. 2019-04-18 07:43:08 -05:00
476a94c679 Add property removal behavior: specifying a property with no value removes it. 2019-01-24 22:30:20 -06:00
65edc56e08 Add delegate command. 2019-01-18 18:51:51 -06:00
d4db66a71e Updates to compile on Nim 0.19 2019-01-17 13:18:25 -06:00
f8ccc831ef WIP Updates to compile on Nim 0.19. 2019-01-17 11:02:46 -06:00
Jonathan Bernard
93a0a15f12 Refactored to move HTTP query params to CLI arguments translation into the cliutils package. 2018-10-01 21:39:35 -05:00
Jonathan Bernard
dc31d590a0 Add GET /issue/<issueId> API endpoint. 2018-10-01 11:22:48 -04:00
Jonathan Bernard
8b46cc19d8 Rename variable to avoid overloading the name. 2018-10-01 11:22:31 -04:00
Jonathan Bernard
567c2d2178 Fix a bug when asking to move an issue to the state it's already in. 2018-06-25 11:40:25 -05:00
Jonathan Bernard
08dfbde57f Add the ability to order issues. 2018-06-11 12:11:26 -05:00
Jonathan Bernard
a924d7b649 Add filters for text-matching on issue summary or details. 2018-06-11 10:19:10 -05:00
Jonathan Bernard
2404f6a3d1 Add the ability to edit all issues in a given state. 2018-06-06 09:43:31 -05:00
Jonathan Bernard
2b5f82203c Add list contexts, refactor display logics.
* Refactor formatting logic to better calculate needed padding between
  issues and sections.
* Add `list contexts` command to list all known contexts according to
  the contexts configuration and the contexts defined in issues.
* Be more intentional about when the default context is used. Don't
  override existing context values in issues when changing their state.
* `edit` now allows multiple issues to be edited.
* Change single-issue display to be more verbose, listing all the
  properties and tags on an issue.
2018-05-29 14:24:18 -05:00
Jonathan Bernard
29959a6a8d Add REST API. Refactor config logic.
The REST API is simply a wrapper around the command line (and actually
invokes the command line). It relies on the command line tool validating
its input.

Currently only the `/list` endpoint is implemented, exposing the `list`
command.
2018-05-18 16:06:58 -05:00
Jonathan Bernard
6f247032a3 Add created property when creating issues. 2018-05-14 17:17:47 -05:00
Jonathan Bernard
efd5f6adff Add versbose flag, list specific issue. 2018-05-14 12:21:05 -05:00
Jonathan Bernard
49c5753ef1 Add rm as an alias for delete. 2018-05-14 10:09:33 -05:00
Jonathan Bernard
3bdb2ecb1f Fix padding issue in context listing. 2018-05-14 10:04:24 -05:00
Jonathan Bernard
28569a643e Added Dormant state, auto-create task dirs.
The Dormant state is for tasks that are still outstanding but not of
immediate importance. The main different between Dormant and Todo is
that dormant tasks are not listed by default. You must
`pit list dormant` to see them.
2018-05-14 09:53:15 -05:00
Jonathan Bernard
97eb286e32 Reorganize project folder structure to work according to nimble package expectations. 2018-05-14 09:36:23 -05:00
Jonathan Bernard
fcab7a4cc6 Added add (alias for new) and delete. 2018-05-14 09:28:50 -05:00
Jonathan Bernard
20c82ea9ba Fix version string. 2018-05-14 09:13:44 -05:00
Jonathan Bernard
11b18317bd Implemented new, edit, state transitions. 2018-05-13 02:58:08 -05:00
Jonathan Bernard
d86da67284 WIP Support basic list options. 2018-05-12 11:40:15 -05:00
Jonathan Bernard
063a869b51 Remove and ignore binary artifact. 2018-05-11 21:36:05 -05:00
Jonathan Bernard
46d4db0d6a Capitalize moved to unicode module. 2018-05-11 21:35:03 -05:00
Jonathan Bernard
34e01119a9 Initial WIP of new pit (Nim). 2018-05-11 18:39:40 -05:00
Jonathan Bernard
d6880d9cc1 Clean slate. 2018-05-11 11:33:28 -05:00
Jonathan Bernard
582bf819f5 Renamed libpit build artifacts. 2016-01-31 00:45:27 -06:00
Jonathan Bernard
76d1e48ebb Fixed a typo in an error message. 2012-08-30 06:26:39 -07:00
Jonathan Bernard
4fd297e03d CLI: Changed the daily list upcoming section behavior.
Upcoming now includes issues due within the next week by default. The number of
days to look ahead is configurable with the --dl-upcoming-days option.
2012-08-30 06:17:07 -07:00
Jonathan Bernard
d0e968b2b7 New jlp-based documentation for pit-cli. 2012-02-13 12:12:26 -06:00
Jonathan Bernard
c0b02ca222 Bugfixes on pit-cli:
* Fixed a bug in the option parsing. When no options (or unknown options) where
  presented it was not properly defaulting.
* Fixed a bug when incorrect options where given. Apache Commons CLI fails
  entirely when it is unable to parse an option. This means we cannot get the
  `--dir` option and we default to the program's working directory. When running
  on Nailgun this is not the desired behavior and can cause pit to look through
  a very deep file heirarchy to find issues.
2012-02-13 10:49:27 -06:00
Jonathan Bernard
85753de955 PIT CLI agenda sorting, FileIssue bug fixed.
* Fixed the bug in FileIssue where it would append a blank line to the body of
  an issue every time it wrote the issue to the file. Fixed by making the
  parser consume a line break before the property section.
* Fixed PIT CLI -D option sorting of issues so that it will sort issues by
  priority, then due date.
2011-12-19 16:25:25 -06:00
Jonathan Bernard
0441f3c510 Added -R option for PIT CLI. 2011-12-19 16:13:15 -06:00
Jonathan Bernard
c01eaa0255 Issue tracker: started issue tracking. 2011-12-18 22:57:19 -06:00
Jonathan Bernard
ec7c07f81f Bug fix in FileIssue.
* Property changes that changed the filename of the underlying file were
  neglecting to update the internal file pointer to the new file.
2011-12-12 15:56:10 -06:00
Jonathan Bernard
952064d903 Fixed FileIssue formatting and extended properties.
* The extended properties table was using the maxKeyLength for both keys and
  values.
* FileIssue was not persisting the file when extended properties were updated.
2011-12-08 16:01:54 -06:00
Jonathan Bernard
31b9802477 Fixed PIT CLI output when setting extended properties. 2011-12-08 15:37:06 -06:00
146 changed files with 1908 additions and 6848 deletions

5
.gitignore vendored
View File

@ -1,3 +1,4 @@
release/
*.sw*
*/build/
nimcache/
/pit
/pit_api

2
.mise.toml Normal file
View File

@ -0,0 +1,2 @@
[tools]
nim = "2.2.0"

138
README.md Normal file
View File

@ -0,0 +1,138 @@
# Personal Issue Tracker
This is [Jonathan Bernard's](mailto:jonathan@jdbernard.com) personal issue
tracker. In it's current form it is essentially a way to keep an curated list of
TODO's, organizing them by workflow category (todo, todo-today, dormant, etc.)
and context (Personal, Work, etc.).
## Categories
`pit` organizes issues into the following workflow categories:
- `current` - actively in progress
- `todo` - to be addressed in the future
- `todo-today` - chosen to be addressed today
- `pending` - blocked by some third party
- `dormant` - long-term things I don't want to forget but don't need in front
of me every day.
- `done`
In my typical workflow the `todo` category serves as a collection point for
things I want to keep track of. Then on a a daily basis I review issues in the
`todo` category and move a selection to the `todo-today` category. I also try
to keep the total number of issues in the `todo` below about a dozen. If there
are more than a dozen things in my `todo` category I will identify the lowest
priority items and move them to the `dormant` category.
## Issue Properties
`pit` allows arbitrary properties to be attached to issues in the form of
key-value pairs. On the command line these can be provided via the `-p` or
`--properties` parameter in the form
`-p <prop1Name>:<prop1Value>;<prop2Name>:<prop2Value>[;...]`
There are a couple of properties that pit will recognize automatically:
- `context`: the context organization feature is implemented using issue
properties.
- `created`: `pit` uses this property to timestamp an issue when it is created.
- `completed`: `pit` uses this property to timestamp an issue when it is moved
to the `done` category.
- `pending`: `pit` looks to this property to provide extra information about
issues in the `pending` category. Typically I use this to note who or what is
blocking the issue and why.
Some other common properties I use are:
- `resolution`: for short notes about why an issue was moved to `done`,
especially if it the action wasn't taken or if it is not completely clear
that this issue was completed.
## Configuration Options
`pit` allows configuration via command-line options and via a configuration
file. There is some overlap between the two methods of configuring `pit`, but
it is not a complete mapping.
### Config File
`pit` looks for a JSON configuration file in the following places (in order):
1. From a file path passed on the command line via the `--config <cfgFile>` parameter,
2. `./.pitrc`, in the current working directory,
3. From a file path set in the `PITRC` environment variable.
4. `$HOME/.pitrc`, in the user's home directory.
#### Sample Config File
This example illustrates all of the possible configuration options.
```json
{
"api": {
"apiKeys": [
"50cdcb660554e2d50fd88bd40b6579717bf00643f6ff57f108baf16c8c083f77",
"e4fc1aac49fc1f2f7f4ca6b1f04d41a4ccdd58e13bb53b41da97703d47267ceb",
]
},
"cli": {
"defaultContext": "personal",
"verbose": false,
"termWidth": 120,
"triggerPtk": true
},
"contexts": {
"nla-music": "New Life Music",
"nla-youth-band": "New Life Youth Band",
"acn": "Accenture",
"hff": "Hope Family Fellowship"
},
"tasksDir": "/mnt/c/Users/Jonathan Bernard/synced/tasks"
}
```
#### Explanation of configurable options.
In general, options supplied on the CLI directly will override options supplied
in the configuration file. All options are optional unless stated otherwise.
* `api`: configuration options specific to the API service.
- `apiKeys`: a list of Bearer tokens accepted by the API for the purpose of
authenticating API requests.
* `cli`: configuration options specific to the CLI.
- `defaultContext`: if present all invokations to the CLI will
be in this context. This is like adding a `--context <defaultContext>`
parameter to every CLI invocation. Any actual `--context` parameter will
override this value.
- `verbose`: Show issue details when listing issues (same as
`--verbose` flag).
- `termWidth`: Set the expected width of the terminal (for wrapping text).
- `triggerPtk`: If set to `true`, invoke the `ptk` command to start and stop
timers when issues move to the `current` and `done` categories
respectively.
* `contexts`: `pit` allows issues to be organized into different contexts via
a `context` property on the issue. The CLI groups issues according to
context. When printing contexts the CLI will take the value from the issues'
`context` properties and capatalize it. In some cases you may wish to have a
different display value for a context. I like to use abbreviations for long
context names to reduce the need to type, `hff` for "Hope Family Fellowship",
for example. The `contexts` config option allows you to provide a map of
context values to context display names See the sample file below for an
example.
Note that this mapping does not have to have entries for all contexts, only
those you wish to provide with an alternate display form. For example, in the
configuration sample above the default context is `personal`, a value not
present in the `contexts` configuration. `personal` will be displayed as
"Personal"; it does not need an alternate display name.
* `tasksDir` **required**: a file path to the root directory for the issue
repository (same as `--tasks-dir` CLI parameter).

View File

@ -1,37 +0,0 @@
<project name="Personal Issue Tracker" default="package">
<property file="version.properties"/>
<property environment="env"/>
<property
name="libpit.jar"
value="libpit/release/pit-${application.version}.jar"/>
<target name="clean">
<ant dir="libpit" target="clean" inheritAll="false"/>
<ant dir="pit-cli" target="clean" inheritAll="false"/>
<!-- <ant dir="pit-swing" target="clean" inheritAll="false"/> -->
</target>
<target name="libpit">
<ant dir="libpit" target="release" inheritAll="false"/>
</target>
<target name="pit-cli" depends="libpit">
<copy file="${libpit.jar}" todir="pit-cli/lib"/>
<ant dir="pit-cli" target="release" inheritAll="false"/>
</target>
<!-- <target name="pit-swing" depends="libpit">
<copy file="${libpit.jar}" todir="pit-swing/lib"/>
<ant dir="pit-swing" fork="true" target="package" inheritAll="false"/>
</target> -->
<!-- <target name="package" depends="libpit,pit-cli,pit-swing"> -->
<target name="package" depends="libpit,pit-cli">
<mkdir dir="release/lib"/>
<copy file="pit-cli/release/pit-clii-${application.version}.jar" todir="release"/>
<!-- <copy file="pit-swing/dist/jar/pit-swing.jar"
tofile="release/pit-swing-${application.version}.jar"/> -->
<copy file="libpit/release/pit-${application.version}.jar" todir="release/lib"/>
</target>
</project>

View File

@ -1,203 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<project name="Jonathan Bernard Build Common">
<property environment="env"/>
<!--======== INIT TARGETS ========-->
<target name="-init" depends="-common-init,init"/>
<target name="-common-init">
<!-- Set default values for some key properties. Since properties are
write once, any value set before this point takes precedence. -->
<property name="versioning.file" value="project.properties"/>
<property name="src.dir" value="${basedir}/src"/>
<property name="build.dir" value="${basedir}/build"/>
<property name="lib.dir" value="${basedir}/lib"/>
<property name="resources.dir" value="${basedir}/resources"/>
<!--======== PATHS ========-->
<path id="groovy.classpath">
<fileset dir="${env.GROOVY_HOME}/lib">
<include name="*.jar"/>
</fileset>
</path>
<path id="groovy.embeddable">
<fileset dir="${env.GROOVY_HOME}/embeddable">
<include name="*.jar"/>
</fileset>
</path>
<path id="compile-libs">
<fileset dir="${build.dir}/lib/compile/jar">
<include name="*.jar"/>
</fileset>
</path>
<path id="runtime-libs">
<fileset dir="${build.dir}/lib/runtime/jar">
<include name="*.jar"/>
</fileset>
</path>
</target>
<target name="-init-groovy">
<taskdef name="groovyc" classpathref="groovy.classpath"
classname="org.codehaus.groovy.ant.Groovyc"/>
<taskdef name="groovy" classpathref="groovy.classpath"
classname="org.codehaus.groovy.ant.Groovy"/>
</target>
<target name="init"/>
<target name="clean" depends="-init">
<delete dir="${build.dir}"/>
</target>
<!--======== LIBRARY TARGETS ========-->
<target name="-lib" depends="-lib-local,-lib-ivy,lib"/>
<target name="lib"/>
<target name="-lib-ivy" unless="${lib.local}"/>
<target name="-lib-local" if="${lib.local}">
<echo message="Resolving libraries locally."/>
<mkdir dir="${build.dir}/lib/compile/jar"/>
<mkdir dir="${build.dir}/lib/runtime/jar"/>
<copy todir="${build.dir}/lib/compile/jar" failonerror="false">
<fileset dir="${lib.dir}/compile/jar"/>
</copy>
<copy todir="${build.dir}/lib/runtime/jar" failonerror="false">
<fileset dir="${lib.dir}/runtime/jar"/>
</copy>
</target>
<!--======== VERSIONING TARGETS ========-->
<target name="increment-build-number" depends="-init">
<propertyfile file="${versioning.file}">
<entry key="build.number" default="0" type="int" value="1"
operation="+"/>
</propertyfile>
</target>
<target name="set-version" depends="-init">
<input
message="The current version is ${version}. Enter a new version: "
addproperty="new-version"/>
<propertyfile file="${versioning.file}">
<entry key="version" value="${new-version}" operation="="
type="string"/>
<entry key="build.number" value="0" type="int" operation="="/>
</propertyfile>
</target>
<!--======== COMPILATION TARGETS ========-->
<target name="-compile-groovy" depends="-init,-init-groovy,-lib">
<mkdir dir="${build.dir}/main/classes"/>
<groovyc srcdir="${src.dir}/main" destdir="${build.dir}/main/classes"
includeAntRuntime="false" fork="yes">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
</classpath>
<javac/>
</groovyc>
</target>
<target name="-compile-java" depends="-init,-lib">
<mkdir dir="${build.dir}/main/classes"/>
<javac srcdir="${src.dir}/main" destdir="${build.dir}/main/classes"
includeAntRuntime="false" classpathref="compile-libs"/>
</target>
<target name="compile" depends="-compile-groovy"/>
<!--======== JUNIT TARGETS ========-->
<target name="-compile-tests-groovy" depends="-init,compile">
<mkdir dir="${build.dir}/test/classes"/>
<groovyc srcdir="${src.dir}/test" destdir="${build.dir}/test/classes"
includeAntRuntime="false" fork="true">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
</classpath>
</groovyc>
</target>
<target name="-compile-tests-java" depends="-init,compile">
<mkdir dir="${build.dir}/test/classes"/>
<javac srcdir="${src.dir}/test" destdir="${build.dir}/test/classes"
includeAntRuntime="false">
<classpath>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
</classpath>
</javac>
</target>
<target name="compile-tests" depends="-compile-tests-groovy"/>
<target name="run-tests" depends="compile-tests,resources-test">
<junit printsummary="true">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
<path location="${build.dir}/test/classes"/>
</classpath>
<formatter type="plain" usefile="false"/>
<batchtest>
<fileset dir="${build.dir}/test/classes">
<include name="**/*"/>
</fileset>
</batchtest>
</junit>
</target>
<!--======== RESOURCES TARGETS ========-->
<target name="resources" depends="-init">
<mkdir dir="${build.dir}/main/classes"/>
<copy todir="${build.dir}/main/classes" failonerror="false">
<fileset dir="${resources.dir}/main/"/>
</copy>
</target>
<target name="resources-test" depends="-init">
<mkdir dir="${build.dir}/test/classes"/>
<copy todir="${build.dir}/test/classes" failonerror="false">
<fileset dir="${resources.dir}/test/"/>
</copy>
</target>
<!--======== BUILD TARGETS ========-->
<target name="-build-modular"
depends="compile,increment-build-number,resources">
<jar destfile="${build.dir}/${name}-${version}.${build.number}.jar"
basedir="${build.dir}/main/classes"/>
</target>
<target name="-build-packed-libs"
depends="compile,increment-build-number,resources">
<unjar destdir="${build.dir}/main/classes">
<fileset dir="${build.dir}/lib/runtime/jar"/>
</unjar>
<jar destfile="${build.dir}/${name}-${version}.${build.number}.jar"
basedir="${build.dir}/main/classes"/>
</target>
<target name="build" depends="-build-modular"/>
</project>

View File

@ -1,23 +0,0 @@
<project name="Personal Issue Tracker" default="release">
<property file="project.properties"/>
<import file="../jdb-build-1.6.xml"/>
<target name="init">
<fail
unless="env.GROOVY_HOME"
message="GROOVY_HOME environment variable is not set."/>
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
</target>
<target name="release" depends="build">
<mkdir dir="${release.dir}/lib"/>
<copy file="${build.dir}/${name}-${version}.${build.number}.jar"
tofile="${release.dir}/${name}-${version}.jar"/>
<copy todir="${release.dir}/lib">
<fileset dir="${build.dir}/lib/runtime/jar"/>
</copy>
</target>
</project>

View File

@ -1,7 +0,0 @@
IssueFile - Title Body PropertyBlock?
Title - ONE_LINE TITLE_SEPARATOR
Body - ANY_LINE+
Separator - DASH{4} NEW_LINE
PropertyBlock - HorizontalRule TableSeparator PropertyDefinition+ TableSeparator
TableSeparator -
PropertyDefinition - PropertyKey COLON PropertyValue

View File

@ -1,13 +0,0 @@
#Thu, 08 Dec 2011 14:35:45 -0600
#Sat Apr 24 17:08:00 CDT 2010
build.dir=build
src.dir=src
lib.shared.dir=../shared-libs
test.dir=test
build.number=10
version=3.2.0
name=libpit
lib.dir=lib
lib.local=true
release.dir=release
release.jar=pit-${application.version}.jar

View File

@ -1,13 +0,0 @@
import com.jdbernard.pit.*
import com.jdbernard.pit.file.*
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.parboiled.parserunners.RecoveringParseRunner
parser = Parboiled.createParser(IssuePegParser.class)
parseRunner = new ReportingParseRunner(parser.IssueFile())
issueFile = new File('/Volumes/NO NAME/Dropbox/tasks/0015tn3.rst')
issueText = issueFile.text
result = parseRunner.run(issueText)
issueMap = result.valueStack.pop()

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
package com.jdbernard.pit
public enum Category {
BUG,
FEATURE,
TASK
public static Category toCategory(String s) {
for(c in Category.values())
if (c.name().startsWith(s.toUpperCase())) return c
throw new IllegalArgumentException("No category matches ${s}.")
}
public String getSymbol() { toString()[0].toLowerCase() }
public String toString() { return "${name()[0]}${name()[1..-1].toLowerCase()}" }
}

View File

@ -1,81 +0,0 @@
package com.jdbernard.pit
import org.joda.time.DateMidnight
import org.joda.time.DateTime
import java.text.SimpleDateFormat
public enum ExtendedPropertyHelp {
// Property types should be ordered here in order of decreasing specificity.
// That is, subclasses should come before the more general class so that
// objects are converted using the most specific class that
// ExtendedPropertyHelp knows how to work with.
DATE_MIDNIGHT(/^\d{4}-\d{2}-\d{2}$/, DateMidnight,
{ v -> DateMidnight.parse(v) },
{ d -> d.toString("YYYY-MM-dd") }),
DATETIME(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, DateTime,
{ v -> DateTime.parse(v) },
{ d -> d.toString("YYYY-MM-dd'T'HH:mm:ss") }),
// We never want to parse a value into a java.util.Date or
// java.util.Calendar object (we are using Joda Time instead of the
// standard Java Date and Calendar objects) but we do want to be able to
// handle if someone gives us a Date or Calendar object.
DATE(NEVER_MATCH, Date,
{ v -> v }, // never called
{ d -> dateFormat.format(d) }),
CALENDAR(NEVER_MATCH, Calendar,
{ v -> v }, // never called
{ c ->
def df = dateFormat.clone()
df.calendar = c
df.format(c.time) }),
INTEGER(NEVER_MATCH, Integer,
{ v -> v as Integer }, // never called
{ i -> i as String }),
LONG(/^\d+$/, Long,
{ v -> v as Long },
{ l -> l as String }),
FLOAT(NEVER_MATCH, Float,
{ v -> v as Float}, // never called
{ f -> f as String}),
DOUBLE(/^\d+\.\d+$/, Double,
{ v -> v as Double },
{ d -> d as String });
String pattern;
Class klass;
def parseFun, formatFun;
private static SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
// This pattern for can never match (is uses negative lookahead to
// contradict itself).
private static String NEVER_MATCH = /(?!x)x/;
public ExtendedPropertyHelp(String pattern, Class klass, def parseFun,
def formatFun) {
this.pattern = pattern
this.klass = klass
this.parseFun = parseFun
this.formatFun = formatFun }
public boolean matches(String prop) { return prop ==~ pattern }
public boolean matches(Class klass) { return this.klass == klass }
public static Object parse(String value) {
def propertyType = ExtendedPropertyHelp.values().find {
it.matches(value) }
return propertyType ? propertyType.parseFun(value) : value }
public static String format(def object) {
def propertyType = ExtendedPropertyHelp.values().find {
it.klass.isInstance(object) }
return propertyType ? propertyType.formatFun(object) : object.toString() }
}

View File

@ -1,42 +0,0 @@
package com.jdbernard.pit
class Filter {
List<Category> categories = null
List<Status> status = null
List<String> projects = null
List<String> ids = null
Map<String, Object> extendedProperties = null
int priority = 9
boolean acceptProjects = true
def issueSorter = defaultIssueSorter
def projectSorter = defaultProjectSorter
public static Closure defaultIssueSorter = { it.id.toInteger() }
public static Closure defaultProjectSorter = { it.name }
public boolean accept(Issue i) {
return (
// Needs to meet the priority threshold.
i.priority <= priority &&
// Needs to be in one of the filtered categories (if given)
(!categories || categories.contains(i.category)) &&
// Needs to have one of the filtered statuses (if given)
(!status || status.contains(i.status)) &&
// Needs to be one of the filtered ids (if given)
(!ids || ids.contains(i.id)) &&
// Needs to have all of the extended properties (if given)
(!extendedProperties ||
extendedProperties.every { name, value -> i[name] == value }))
}
public boolean accept(Project p) {
return (acceptProjects &&
(!projects || projects.contains(p.name)))
}
public boolean accept(String name) {
return (acceptProjects &&
(!projects || projects.contains(name)))
}
}

View File

@ -1,37 +0,0 @@
package com.jdbernard.pit
public class FlatProjectView extends Project {
public FlatProjectView(String name) { super(name) }
public Issue createNewIssue(Map options) {
throw new UnsupportedOperationException("The FlatProjectView is " +
"read-only.")
}
public Project createNewProject(String name) {
throw new UnsupportedOperationException("The FlatProjectView is " +
"read-only.")
}
public boolean deleteIssue(Issue issue) { return false }
public boolean deleteProject(Project project) { return false }
public boolean delete() { return true }
public void eachIssue(Filter filter = null, Closure closure) {
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
def gatherIssues
def gatheredIssues = []
gatherIssues = { project, f ->
project.eachIssue(f) { gatheredIssues << it }
project.eachProject(f) { gatherIssues(it, f) }
}
for (p in projects.values())
if (!filter || filter.accept(p))
gatherIssues(p, filter)
gatheredIssues.sort(sorter).each(closure)
}
}

View File

@ -1,74 +0,0 @@
package com.jdbernard.pit
import java.lang.IllegalArgumentException as IAE
public class Issue {
protected String id
protected Category category
protected Status status
protected int priority
protected String text
protected String title
Map extendedProperties = [:]
Issue(Map props) {
this.id = props.id
this.category = props.category ?: Category.TASK
this.status = props.status ?: Status.NEW
this.priority = props.priority ?: 5
this.title = props.title ?: ''
this.text = props.text ?: ''
// Put all the non-native properties into our extendedProperties map.
def nativeProps =
["id", "category", "status", "priority", "title", "text"]
extendedProperties.putAll(props.findAll {
!nativeProps.contains(it.key) })}
public String getId() { return id; }
public Category getCategory() { return category }
public void setCategory(Category c) throws IOException {
if (c == null)
throw new IAE("Category cannot be null.")
this.category = c
}
public Status getStatus() { return status }
public void setStatus(Status s) throws IOException {
if (s == null)
throw new IAE("Status cannot be null.")
this.status = s
}
public int getPriority() { return priority }
public void setPriority(int p) throws IOException {
priority = Math.min(9, Math.max(0, p))
}
public String getTitle() { return title }
public void setTitle(String t) throws IOException { title = t }
public String getText() { return text }
public void setText(String t) throws IOException { text = t }
public def propertyMissing(String name) { extendedProperties[name] }
public def propertyMissing(String name, def value) {
extendedProperties[name] = value }
@Override
public String toString() {
return "${id}(${priority}-${status}): ${category} ${title}"
}
}

View File

@ -1,60 +0,0 @@
package com.jdbernard.pit
public abstract class Project {
protected String name
Map<String, Issue> issues = [:]
Map<String, Project> projects = [:]
Project(String name) { this.name = name }
public void eachIssue(Filter filter = null, Closure c) {
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
for (i in sort(issues.values(), sorter))
if (!filter || filter.accept(i))
c.call(i) }
public void eachProject(Filter filter = null, Closure c) {
def sorter = filter?.projectSorter ?: Filter.defaultProjectSorter
for (p in sort(projects.values(), sorter))
if (!filter || filter.accept(p))
c.call(p) }
// walk every issue and project in this project recursively and execute the
// given closure on each issue that meets the filter criteria
public void walkProject(Filter filter, Closure c) {
this.eachIssue(filter, c)
this.eachProject(filter) { p -> p.walkProject(filter, c) } }
// This get all issues, including subissues
public List getAllIssues(Filter filter = null) {
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
List allIssues = this.issues.values().findAll {
filter ? filter.accept(it) : true }
this.eachProject(filter) { p -> allIssues += p.getAllIssues(filter) }
return sort(allIssues, sorter) }
public void setName(String name) { this.name = name }
public String getName() { return name }
@Override
String toString() { return name }
public abstract Issue createNewIssue(Map options)
public abstract Project createNewProject(String name)
public abstract boolean deleteIssue(Issue issue)
public abstract boolean deleteProject(Project project)
protected List sort(def collection, def sorter) {
if (sorter instanceof Closure) {
return collection.sort(sorter) }
else if (sorter instanceof List) {
return sorter.reverse().inject(collection) { c, s -> c.sort(s) }}}
}

View File

@ -1,8 +0,0 @@
package com.jdbernard.pit
public abstract class Repository {
public abstract void persist()
public abstract Project[] getRootProjects()
public abstract Project createNewProject(String name)
}

View File

@ -1,41 +0,0 @@
package com.jdbernard.pit
public enum Status {
REASSIGNED('a'),
REJECTED('j'),
NEW('n'),
RESOLVED('s'),
VALIDATION_REQUIRED('v')
String symbol
protected Status(String s) { symbol = s }
public static Status toStatus(String str) {
// Try to match based on symbol
def match = Status.values().find {it.symbol.equalsIgnoreCase(str)}
if (match) { return match }
// No match on the symbol, look for the status name (or abbreviations)
match = Status.values().findAll {
it.name().startsWith(str.toUpperCase()) }
// No matching status, oops.
if (match.size() == 0) {
throw new IllegalArgumentException("No status matches '${str}'") }
// More than one matching status, oops.
else if (match.size() > 1) {
throw new IllegalArgumentException("Request string is" +
" ambigous, '${str}' could represent any of ${match}.")}
// Only one matching status, yay!
else { return match[0] }}
public String toString() {
def words = name().split("_")
String result = ""
words.each { result += "${it[0]}${it[1..-1].toLowerCase()} " }
return result[0..-2]
}
}

View File

@ -1,183 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
import java.lang.IllegalArgumentException as IAE
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public class FileIssue extends Issue {
protected File source
private Logger log = LoggerFactory.getLogger(getClass())
public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/
protected static parseRunner
static {
def parser = Parboiled.createParser(IssuePegParser)
parseRunner = new ReportingParseRunner(parser.IssueFile()) }
public FileIssue(File file) {
super(id: -1, title: 'REPLACE_ME')
if (log.isDebugEnabled()) {
log.debug("Loading a FileIssue from '{}'", file.canonicalPath) }
def matcher = file.name =~ fileExp
if (!matcher)
throw new IllegalArgumentException("${file} " +
"is not a valid Issue file.")
// Read issue attributes from the filename.
super.id = matcher[0][1]
super.category = Category.toCategory(matcher[0][2])
super.status = Status.toStatus(matcher[0][3])
super.priority = matcher[0][4].toInteger()
log.debug("id: {}\tcategory: {}\tstatus: {}\tpriority: {}",
super.id, super.category, super.status, super.priority)
this.source = file
// Parse the file and extract the title, text, and extended properties
// TODO: guard against parsing problems (null/empty value stack, etc.)
def parsedIssue = parseRunner.run(file.text).valueStack.pop()
super.text = parsedIssue.body
super.title = parsedIssue.title
// Add the extended properties
parsedIssue.extProperties.each { key, value ->
key = key.toLowerCase().replaceAll(/\s/, '_')
super.extendedProperties[key] =
ExtendedPropertyHelp.parse(value) }
}
public void setCategory(Category c) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, c, status, priority)))
if (!renamed)
throw new IOException("I was unable to set the category. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setCategory(c) }
public void setStatus(Status s) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, category, s, priority)))
if (!renamed)
throw new IOException("I was unable to set the status. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setStatus(s) }
public void setPriority(int p) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, category, status, p)))
if (!renamed)
throw new IOException("I was unable to set the priority. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setPriority(p) }
public String getFilename() {
return makeFilename(id, category, status, priority) }
public void setTitle(String title) throws IOException {
super.setTitle(title)
writeFile() }
public void setText(String text) throws IOException {
super.setText(text)
writeFile() }
boolean deleteFile() { return source.deleteDir() }
public static boolean isValidFilename(String name) {
return name ==~ fileExp }
public static String makeFilename(String id, Category category,
Status status, int priority) {
// bounds check priority
priority = Math.min(9, Math.max(0, priority))
//check for valid values of cateogry and id
if (category == null)
throw new IAE("Category must be non-null.")
if (status == null)
throw new IAE("Status must be non-null.")
if (!(id ==~ /\d+/))
throw new IAE( "'${id}' is not a legal value for id.")
return id + category.symbol + status.symbol + priority + ".rst" }
public static String formatIssue(Issue issue) {
def result = new StringBuilder()
result.append(issue.title)
result.append("\n")
result.append("=".multiply(issue.title.length()))
result.append("\n\n")
result.append(issue.text)
// If there are any extended properties, let's write those.
if (issue.extendedProperties.size() > 0) {
result.append("\n----\n\n")
def extOutput = [:]
def maxKeyLen = 0
def maxValLen = 0
// Find the longest key and value, convert all to strings.
issue.extendedProperties.each { key, val ->
def ks = key.toString().split('_').collect({it.capitalize()}).join(' ')
def vs = ExtendedPropertyHelp.format(val)
extOutput[ks] = vs
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
if (vs.length() > maxKeyLen) { maxValLen = vs.length() } }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n")
extOutput.sort().each { key, val ->
result.append(key.padRight(maxKeyLen))
result.append(": ")
result.append(val.padRight(maxValLen))
result.append("\n") }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n") }
return result.toString()}
protected void writeFile() {
try { source.write(formatIssue(this)) }
catch (IOException ioe) {
throw new IOException("I could not save the new text for this "
+ "issue. I can not write to the file for this issue. I do not"
+ " know why, I am sorry (maybe the file can not be reached).") } }
}

View File

@ -1,107 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
class FileProject extends Project {
protected File source
public FileProject(File dir) {
super(dir.canonicalFile.name)
if (!dir.isDirectory())
throw new IllegalArgumentException(
"${dir.name} is not a directory.")
this.source = dir
dir.eachFile { child ->
// add sub projects
if (child.isDirectory()) {
if (child.name ==~ /\d+/ ||
child.isHidden()) return // just an issue folder
// otherwise build and add to list
projects[(child.name)] = new FileProject(child) }
else if (child.isFile() &&
FileIssue.isValidFilename(child.name)) {
def issue
// if exception, then not an issue
try { issue = new FileIssue(child) } catch (all) { return }
issues[(issue.id)] = issue } }}
public void setName(String name) {
super.setName(name)
source.renameTo(new File(source.canonicalFile.parentFile, name)) }
public FileIssue createNewIssue(Map options) {
Issue issue
File issueFile
if (!options) options = [:]
// We want some different defaults for issues due to the parser being
// unable to handle empty title or text.
if (!options.title) options.title = "Default issue title."
if (!options.text) options.text = "Describe the issue here."
// We are also going to find the next id based on the issues already in the
// project.
if (issues.size() == 0) options.id = '0000'
else {
def lastId = (issues.values().max { it.id.toInteger() }).id
options.id = (lastId.toInteger() + 1).toString().padLeft(
lastId.length(), '0') }
// Create an Issue object from the options (we will discard it later).
issue = new Issue(options)
// Create the filename and File object based on the options given.
issueFile = new File(source, FileIssue.makeFilename(
issue.id, issue.category, issue.status, issue.priority))
// Create the actual file on the system
issueFile.createNewFile()
// Write the issue to the file created.
issueFile.write(FileIssue.formatIssue(issue))
// Read that new file back in as a FileIssue
issue = new FileIssue(issueFile)
// Add the issue to our collection.
issues[(issue.id)] = issue
return issue }
public FileProject createNewProject(String name) {
def newDir = new File(source, name)
newDir.mkdirs()
return new FileProject(newDir) }
public boolean deleteIssue(Issue issue) {
if (!issues[(issue.id)]) return false
issues.remove(issue.id)
if (issue instanceof FileIssue)
return issue.deleteFile()
else return true }
public boolean deleteProject(Project project) {
if (!projects[(project.name)]) return false
projects.remove(project.name)
if (project instanceof FileProject)
return project.source.delete()
return true }
@Override
public String toString() { return name }
}

View File

@ -1,22 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
public class FileRepository extends Repository {
@Delegate FileProject fileProject
public FileRepository(File dir) {
assert dir.isDirectory()
fileProject = new FileProject(dir)
}
public void persist() {} // nothing to do
public Project[] getRootProjects() {
return [fileProject] as Project[]
}
public FileProject createNewProject(String name) {
return fileProject.createNewProject()
}
}

View File

@ -1,67 +0,0 @@
package com.jdbernard.pit.file;
import java.util.HashMap;
import java.util.Map;
import org.parboiled.Action;
import org.parboiled.BaseParser;
import org.parboiled.Context;
import org.parboiled.Rule;
import org.parboiled.annotations.*;
@BuildParseTree
public class IssuePegParser extends BaseParser<Object> {
public Rule IssueFile() {
return Sequence(push(makeNode()),
Title(), Body(), Optional(PropertyBlock())); }
Rule Title() {
return Sequence(
OneOrMore(NOT_EOL), addToNode("title", match()), EOL,
HorizontalRule(), EOL,
ZeroOrMore(SPACE), EOL); }
Rule Body() { return Sequence(OneOrMore(Sequence(
TestNot(PropertyBlock()), ANY)), addToNode("body", match())); }
Rule PropertyBlock() {
return Sequence(push(makeNode()),
HorizontalRule(), OneOrMore(EOL), TableSeparator(), EOL,
OneOrMore(PropertyDefinition()), TableSeparator(),
addToNode("extProperties", pop())); }
Rule PropertyDefinition() {
return Sequence(
PropertyKey(), push(match()), COLON,
PropertyValue(), push(match()), EOL,
swap(), addToNode(popAsString().trim(), popAsString().trim())); }
Rule PropertyKey() { return OneOrMore(Sequence(TestNot(COLON), NOT_EOL)); }
Rule PropertyValue() { return OneOrMore(NOT_EOL); }
Rule TableSeparator() {
return Sequence(OneOrMore(SEPARATOR_CHAR), OneOrMore(SPACE),
OneOrMore(SEPARATOR_CHAR)); }
Rule HorizontalRule() {
return Sequence(SEPARATOR_CHAR, SEPARATOR_CHAR, SEPARATOR_CHAR,
OneOrMore(SEPARATOR_CHAR)); }
Rule EOL = Ch('\n');
Rule NOT_EOL = Sequence(TestNot(EOL), ANY);
Rule SEPARATOR_CHAR = AnyOf("\"'`~:-_=+*^#<>");
Rule SPACE = AnyOf(" \t");
Rule COLON = Ch(':');
Map makeNode() { return new HashMap(); }
boolean addToNode(Object key, Object val) {
Map node = (Map) pop();
node.put(key, val);
push(node);
return true; }
String popAsString() { return (String) pop(); }
}

View File

@ -1,36 +0,0 @@
package com.jdbernard.pit.util
import com.jdbernard.pit.*
if (args.size() != 1) {
println "Usage: Convert1_2 [dir]"
System.exit(1)
}
File rootDir = new File(args[0])
Scanner scan = new Scanner(System.in)
rootDir.eachFileRecurse { file ->
def m = file.name =~ /(\d+)([bcft])(\d).*/
if (m && file.isFile()) {
println m[0][0]
def parentFile = file.canonicalFile.parentFile
def c
def s
switch(m[0][2]) {
case "c":
println file.readLines()[0]
print "Issue was closed, was category does it belong in?"
c = Category.toCategory(scan.nextLine())
s = Status.RESOLVED
break
default:
c = Category.toCategory(m[0][2])
s = Status.NEW
break
}
println "${m[0][2]}: ${c}"
file.renameTo(new File(parentFile,
FileIssue.makeFilename(m[0][1], c, s, m[0][3].toInteger())))
}
}

View File

@ -1,74 +0,0 @@
package com.jdbernard.pit.xml
import com.jdbernard.pit.*
public class XmlIssue extends Issue {
def issueNode
XmlProject project
XmlRepository repository
XmlIssue(def issueNode, XmlRepository repository, XmlProject project) {
super(issueNode.@id, issueNode.@category ?: Category.TASK,
issueNode.@status ?: Status.NEW, issueNode.@priority ?: 9)
this.issueNode = issueNode
this.project = project
this.repository = repository
}
XmlIssue(String id, Category c = Category.TASK, Status s = Status.NEW,
int p = 9, String title, String text, XmlRepository repository,
XmlProject project) {
super(id, c, s, p)
this.project = project
this.repository = repository
// Node constructor adds the node to the parent node
issueNode = new Node(project.projectNode, "Issue",
[id: id, category: c, status: s, priority: p, title: title])
super.@title = title
super.@text = text
issueNode.value = text
repository.persist()
}
public void setCategory(Category c) {
super.setCategory(c)
issueNode.@category = c.name()
repository.persist()
}
public void setStatus(Status s) {
super.setStatus(s)
issueNode.@status = s.name()
repository.persist()
}
public void setPriority(int p) {
super.setPriority(p)
issueNode.@priority = p
repository.persist()
}
public void setText(String t) {
super.setText(t)
issueNode.value = t
repository.persist()
}
public void setTitle(String t) {
super.setTitle(t)
issueNode.@title = t
repository.persist()
}
}

View File

@ -1,83 +0,0 @@
package com.jdbernard.pit.xml
import com.jdbernard.pit.*
public class XmlProject extends Project {
def projectNode
XmlRepository repository
XmlProject(def projectNode, XmlRepository repository) {
super(projectNode.@name)
this.projectNode = projectNode
this.repository = repository
}
XmlProject(String name, def parentProject, XmlRepository repository) {
super(name)
// Node constructor adds the node to the parent node
projectNode = new Node(parentProject.projectNode, "Project",
[name: name])
repository.persist()
}
public void setName(String name) {
super.setName(name)
projectNode.@name = name
repository.persist()
}
public XmlIssue createNewIssue(Map options) {
if (!options) options = [:]
if (!options.category) options.category = Category.TASK
if (!options.status) options.status = Status.NEW
if (!options.priority) options.priority = 5
if (!options.text) options.text = "Default issue title.\n" +
"====================\n"
String id
if (issues.size() == 0) id = "0000"
else {
id = (issues.values().max { it.id.toInteger() }).id
id = (id.toInteger() + 1).toString().padLeft(id.length(), '0')
}
// XmlIssue constructor will persist XML data
issues[(id)] = new XmlIssue(id, options.category, options.status,
options.priority, options.text, repository, this)
return issues[(id)]
}
public XmlProject createNewProject(String name) {
// XmlProject constructor persists the XML data
projects[(name)] = new XmlProject(name, this, repository)
return projects[(name)]
}
public boolean deleteIssue(Issue issue) {
if (!issues[(issue.id)]) return false
issues.remove(issue.id)
if (issue instanceof XmlIssue)
projectNode.remove(issue.issueNode)
repository.persist()
return true
}
public boolean deleteProject(Project project) {
if (!projects[(project.name)]) return false
projects.remove(project.name)
if (project instanceof XmlProject)
projectNode.remove(project.projectNode)
repository.persist()
}
}

View File

@ -1,47 +0,0 @@
package com.jdbernard.pit.xml
import com.jdbernard.pit.*
import groovy.xml.XmlUtil
public class XmlRepository extends Repository {
def repository
def projects = []
File repoFile
public XmlRepository(File repoFile) {
this.repoFile = repoFile
repository = new XmlParser().parse(repoFile)
repository.Project.each { projectNode ->
projects << new XmlProject(projectNode)
}
}
public synchronized void persist() {
repoFile.withOutputStream { XmlUtil.serialize(repository, it) }
}
public Project[] getRootProjects() {
return projects as XmlProject[]
}
public XmlProject createNewProject(String name) {
def newProject = new XmlProject(name, this, null)
repository << newProject.projectNode
persist()
return newProject
}
public boolean deleteProject(Project p) {
if (!projects.contains(p)) return false
projects.remove(p)
repository.remove(p.projectNode)
return true
}
}

View File

@ -1,32 +0,0 @@
package com.jdbernard.pit
import org.junit.Test
import static org.junit.Assert.assertEquals
import static com.jdbernard.pit.Category.toCategory
class CategoryTest {
@Test void testToCategory() {
assertEquals toCategory("BUG"), Category.BUG
assertEquals toCategory("FEATURE"), Category.FEATURE
assertEquals toCategory("TASK"), Category.TASK
assertEquals toCategory("bug"), Category.BUG
assertEquals toCategory("feature"), Category.FEATURE
assertEquals toCategory("task"), Category.TASK
assertEquals toCategory("b"), Category.BUG
assertEquals toCategory("f"), Category.FEATURE
assertEquals toCategory("t"), Category.TASK
}
@Test void testGetSymbol() {
assertEquals Category.BUG.symbol, "b"
assertEquals Category.FEATURE.symbol, "f"
assertEquals Category.TASK.symbol, "t"
}
}

View File

@ -1,127 +0,0 @@
package com.jdbernard.pit
import org.junit.Test
import org.junit.Before
import org.junit.After
import static org.junit.Assert.assertTrue
import static org.junit.Assert.assertFalse
class FilterTest {
Project proj
@Before void setUpIssues() {
proj = new MockProject('proj1')
def issue = new MockIssue( '0000', Category.TASK, Status.NEW, 5)
proj.issues['0000'] = issue
issue = new MockIssue('0001', Category.BUG, Status.REJECTED, 3)
proj.issues['0001'] = issue
issue = new MockIssue('0002', Category.BUG, Status.RESOLVED, 9)
proj.issues['0002'] = issue
issue = new MockIssue('0003', Category.FEATURE, Status.REASSIGNED, 0)
proj.issues['0003'] = issue
def subProj = new MockProject('subproj1')
proj.projects['subproj1'] = subProj
subProj = new MockProject('subproj2')
proj.projects['subproj2'] = subProj
}
@Test void testDefaultFilter() {
Filter f = new Filter()
proj.issues.values().each { assertTrue f.accept(it) }
proj.projects.values().each { assertTrue f.accept(it) }
}
@Test void testPriorityIssueFilter() {
Filter f = new Filter(priority: 9)
proj.eachIssue { assertTrue f.accept(it) }
f.priority = 6
assertTrue f.accept(proj.issues['0000'])
assertTrue f.accept(proj.issues['0001'])
assertFalse f.accept(proj.issues['0002'])
assertTrue f.accept(proj.issues['0003'])
f.priority = 5
assertTrue f.accept(proj.issues['0000'])
assertTrue f.accept(proj.issues['0001'])
assertFalse f.accept(proj.issues['0002'])
assertTrue f.accept(proj.issues['0003'])
f.priority = 0
assertFalse f.accept(proj.issues['0000'])
assertFalse f.accept(proj.issues['0001'])
assertFalse f.accept(proj.issues['0002'])
assertTrue f.accept(proj.issues['0003'])
}
@Test void testCategoryFilter() {
Filter f = new Filter(categories:
[Category.BUG, Category.FEATURE])
assertFalse f.accept(proj.issues['0000'])
assertTrue f.accept(proj.issues['0001'])
assertTrue f.accept(proj.issues['0002'])
assertTrue f.accept(proj.issues['0003'])
f.categories = [ Category.TASK ]
assertTrue f.accept(proj.issues['0000'])
assertFalse f.accept(proj.issues['0001'])
assertFalse f.accept(proj.issues['0002'])
assertFalse f.accept(proj.issues['0003'])
f.categories = [ Category.BUG, Category.TASK ]
assertTrue f.accept(proj.issues['0000'])
assertTrue f.accept(proj.issues['0001'])
assertTrue f.accept(proj.issues['0002'])
assertFalse f.accept(proj.issues['0003'])
}
@Test void testStatusFilter() {
Filter f = new Filter(status:
[Status.NEW, Status.REASSIGNED, Status.REJECTED])
assertTrue f.accept(proj.issues['0000'])
assertTrue f.accept(proj.issues['0001'])
assertFalse f.accept(proj.issues['0002'])
assertTrue f.accept(proj.issues['0003'])
f.status = [ Status.RESOLVED ]
assertFalse f.accept(proj.issues['0000'])
assertFalse f.accept(proj.issues['0001'])
assertTrue f.accept(proj.issues['0002'])
assertFalse f.accept(proj.issues['0003'])
f.status = [ Status.NEW, Status.RESOLVED ]
assertTrue f.accept(proj.issues['0000'])
assertFalse f.accept(proj.issues['0001'])
assertTrue f.accept(proj.issues['0002'])
assertFalse f.accept(proj.issues['0003'])
}
@Test void testProjectFilter() {
}
@Test void testAcceptsProjectsFilter() {
}
@Test void testCompositeFilter() {
}
}

View File

@ -1,8 +0,0 @@
package com.jdbernard.pit
public class MockIssue extends Issue {
public MockIssue(String id, Category c, Status s, int p) {
super ([id: id, category: c, status: s, priority: p])
}
public boolean delete() { return true }
}

View File

@ -1,20 +0,0 @@
package com.jdbernard.pit
class MockProject extends Project {
public MockProject(String name) { super(name) }
public Issue createNewIssue(Map options) {
return new MockIssue(options.id ?: 'n/a',
options.c ?: Category.TASK, options.s ?: Status.NEW,
options.p ?: 5)
}
public Project createNewProject(String name) {
return new MockProject(name)
}
public boolean delete() { return true }
public boolean deleteProject(Project project) { return true }
public boolean deleteIssue(Issue issue) { return true }
}

View File

@ -1,12 +0,0 @@
package com.jdbernard.pit
class MockRepository extends Repository {
public void persist() {}
public Project[] getRootProjects() { return [] as Project[] }
public Project createNewProject(String name) {
return new MockProject(name)
}
}

View File

@ -1,54 +0,0 @@
package com.jdbernard.pit
import org.junit.Test
import static org.junit.Assert.assertEquals
import static com.jdbernard.pit.Status.toStatus
public class StatusTest {
@Test void testToStatus() {
assertEquals Status.REASSIGNED, toStatus('REASSIGNED')
assertEquals Status.REJECTED, toStatus('REJECTED')
assertEquals Status.NEW, toStatus('NEW')
assertEquals Status.RESOLVED , toStatus('RESOLVED')
assertEquals Status.VALIDATION_REQUIRED,
toStatus('VALIDATION_REQUIRED')
assertEquals Status.REASSIGNED, toStatus('REA')
assertEquals Status.REJECTED, toStatus('REJ')
assertEquals Status.NEW, toStatus('NEW')
assertEquals Status.RESOLVED , toStatus('RES')
assertEquals Status.VALIDATION_REQUIRED,
toStatus('VAL')
assertEquals Status.REASSIGNED, toStatus('reassigned')
assertEquals Status.REJECTED, toStatus('rejected')
assertEquals Status.NEW, toStatus('new')
assertEquals Status.RESOLVED , toStatus('resolved')
assertEquals Status.VALIDATION_REQUIRED,
toStatus('validation_required')
assertEquals Status.REASSIGNED, toStatus('rea')
assertEquals Status.REJECTED, toStatus('rej')
assertEquals Status.NEW, toStatus('new')
assertEquals Status.RESOLVED , toStatus('res')
assertEquals Status.VALIDATION_REQUIRED,
toStatus('val')
assertEquals Status.REASSIGNED, toStatus('A')
assertEquals Status.REJECTED, toStatus('J')
assertEquals Status.NEW, toStatus('N')
assertEquals Status.RESOLVED , toStatus('S')
assertEquals Status.VALIDATION_REQUIRED, toStatus('V')
assertEquals Status.REASSIGNED, toStatus('a')
assertEquals Status.REJECTED, toStatus('j')
assertEquals Status.NEW, toStatus('n')
assertEquals Status.RESOLVED , toStatus('s')
assertEquals Status.VALIDATION_REQUIRED, toStatus('v')
}
}

View File

@ -1,228 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
import org.junit.*
import static org.junit.Assert.assertTrue
import static org.junit.Assert.assertFalse
import static org.junit.Assert.assertEquals
class FileIssueTest {
def issues
File testDir
@Before void makeIssueFiles() {
File issueFile
issues = []
testDir = new File('testdir')
testDir.mkdirs()
issueFile = new File(testDir, '0001fn1.rst')
issueFile.write(
"Add the killer feature to the killer app.\n" +
"=========================================\n\n" +
"Make our killer app shine!.")
issues << new FileIssue(issueFile)
issueFile = new File(testDir, '0002ts5.rst')
issueFile.write(
"Obtain donuts.\n" +
"==============\n\n" +
"The office is seriously lacking in sugary donuts.\n\n" +
"We must rectify this at once!")
issues << new FileIssue(issueFile)
}
@After void deleteIssueFiles() {
assert testDir.deleteDir()
}
@Test void testSetCategory() {
assertEquals issues[0].category, Category.FEATURE
assertEquals issues[1].category, Category.TASK
try {
issues[0].category = Category.TASK
issues[1].category = Category.BUG
} catch (Exception e) {
Assert.fail("An unexpected Exception occurred: "
+ e.getLocalizedMessage())
}
assertEquals issues[0].category, Category.TASK
assertEquals issues[1].category, Category.BUG
assertTrue new File(testDir, '0001tn1.rst').exists()
assertTrue new File(testDir, '0002bs5.rst').exists()
assertFalse new File(testDir, '0001fn1.rst').exists()
assertFalse new File(testDir, '0002ts5.rst').exists()
}
@Test void testSetCategoryFails() {
FileInputStream fin
try {
// get a lock to the file to prevent the rename
def issueFile = new File('0001fn1.rst')
fin = new FileInputStream(issueFile)
// try to set the category
issues[0].category = Category.TASK
// should throw IOE before here
Assert.fail()
} catch (IOException ioe) {
} catch (Exception e) {
Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
} finally {
if (fin != null) fin.close()
}
}
@Test void testSetStatus() {
assertEquals issues[0].status, Status.NEW
assertEquals issues[1].status, Status.RESOLVED
try {
issues[0].status = Status.RESOLVED
issues[1].status = Status.REJECTED
} catch (Exception e) {
Assert.fail("An unexpected Exception occurred: "
+ e.getLocalizedMessage())
}
assertTrue new File(testDir, '0001fs1.rst').exists()
assertTrue new File(testDir, '0002tj5.rst').exists()
assertFalse new File(testDir, '0001fn1.rst').exists()
assertFalse new File(testDir, '0002ts5.rst').exists()
}
@Test void testSetStatusFails() {
FileInputStream fin
try {
// get a lock to the file to prevent the rename
def issueFile = new File('0001fn1.rst')
fin = new FileInputStream(issueFile)
// try to set the status
issues[0].status = Status.REJECTED
// should throw IOE before here
Assert.fail()
} catch (IOException ioe) {
} catch (Exception e) {
Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
} finally {
if (fin != null) fin.close()
}
}
@Test void testSetPriority() {
assertEquals issues[0].priority, 1
assertEquals issues[1].priority, 5
try {
issues[0].priority = 2
issues[1].priority = 9
} catch (Exception e) {
Assert.fail("An unexpected Exception occurred: "
+ e.getLocalizedMessage())
}
assertEquals issues[0].priority, 2
assertEquals issues[1].priority, 9
assertTrue new File(testDir, '0001fn2.rst').exists()
assertTrue new File(testDir, '0002ts9.rst').exists()
assertFalse new File(testDir, '0001fn1.rst').exists()
assertFalse new File(testDir, '0002ts5.rst').exists()
}
@Test void testSetPriorityFails() {
FileInputStream fin
try {
// get a lock to the file to prevent the rename
def issueFile = new File('0001fn1.rst')
fin = new FileInputStream(issueFile)
// try to set the priority
issues[0].priority = 9
// should throw IOE before here
Assert.fail()
} catch (IOException ioe) {
} catch (Exception e) {
Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
} finally {
if (fin != null) fin.close()
}
}
@Test void testConstruction() {
File issueFile = new File(testDir, '0001fn1.rst')
Issue issue = new FileIssue(issueFile)
assertEquals issue.id , "0001"
assertEquals issue.category , Category.FEATURE
assertEquals issue.status , Status.NEW
assertEquals issue.priority , 1
assertEquals issue.title , "Add the killer feature to the killer app."
assertEquals issue.text , "Make our killer app shine!."
assertEquals issue.source , issueFile
}
@Test void testSetTextFails() {
try {
// make the issue file un-writable
def issueFile = new File('0001fn1.rst')
if (issueFile.setReadOnly()) {
// try to write something
issues[0].text = "This should fail to be written."
// should throw IOE before here
Assert.fail()
} else {
println "Could not run testSetTextFails, unable to change " +
"the test isseu file's permissions."
}
} catch (IOException ioe) {
} catch (Exception e) {
Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
}
}
@Test void testMakeFilename() {
assertEquals FileIssue.makeFilename('0001', Category.BUG,
Status.NEW, 5), '0001bn5.rst'
assertEquals FileIssue.makeFilename('0010', Category.FEATURE,
Status.REASSIGNED, 1), '0010fa1.rst'
assertEquals FileIssue.makeFilename('0002', Category.FEATURE,
Status.REJECTED, 3), '0002fj3.rst'
assertEquals FileIssue.makeFilename('0001', Category.BUG,
Status.RESOLVED, -2), '0001bs0.rst'
assertEquals FileIssue.makeFilename('0001', Category.TASK,
Status.VALIDATION_REQUIRED, 10) , '0001tv9.rst'
assertEquals FileIssue.makeFilename('00101', Category.BUG,
Status.NEW, 5), '00101bn5.rst'
try {
FileIssue.makeFilename('badid', Category.BUG, Status.NEW, 5)
assertTrue 'Issue.makeFilename() succeeded with bad id input.', false
} catch (IllegalArgumentException iae) {}
try {
FileIssue.makeFilename('0002', null, Status.NEW, 5)
assertTrue 'Issue.makeFilename() succeeded given no Category.', false
} catch (IllegalArgumentException iae) {}
try {
FileIssue.makeFilename('0002', Category.BUG, null, 5)
assertTrue 'Issue.makeFilename() succeeded given no Status.', false
} catch (IllegalArgumentException iae) {}
}
}

View File

@ -1,162 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertFalse
import static org.junit.Assert.assertNotNull
import static org.junit.Assert.assertTrue
class FileProjectTest {
File testDir
Project rootProj
@Before void createTestProjects() {
testDir = new File('testdir')
assert !testDir.exists()
testDir.mkdirs()
/* TEST SUITE:
/testdir/
0001t5.rst
0002b5.rst
0003f2.rst
subproj1/
0001f3.rst
0002b4.rst
emptyproj/
*/
def issueFile = new File(testDir, '0001tn5.rst')
issueFile.createNewFile()
issueFile.write('Test Issue 1\n' +
'============\n\n' +
'This is the first test issue.')
issueFile = new File(testDir, '0002ba5.rst')
issueFile.createNewFile()
issueFile.write('Test Bug\n' +
'========\n\n' +
'Yeah, it is a test bug.')
issueFile = new File(testDir, '0003fs2.rst')
issueFile.createNewFile()
issueFile.write('Important Feature Request\n' +
'=========================\n\n' +
'Here is our sweet feature. Please implement it!')
def subDir = new File(testDir, 'subproj1')
subDir.mkdirs()
issueFile = new File(subDir, '0001fv3.rst')
issueFile.createNewFile()
issueFile.write('First feature in subproject\n' +
'===========================\n\n' +
'Please make the grubblers grobble.')
issueFile = new File(subDir, '0002bj4.rst')
issueFile.createNewFile()
issueFile.write('Zippners are not zippning.\n' +
'==========================\n\n' +
'For some reason, the Zippners are bilperring, not zippning.')
subDir = new File(testDir, 'emptyproj')
subDir.mkdirs()
rootProj = new FileProject(testDir)
}
@After void deleteTestProjects() {
assert testDir.deleteDir()
if (rootProj.source.exists())
assert rootProj.source.deleteDir()
}
@Test void testConstruction() {
Project proj = new FileProject(testDir)
assertEquals proj.name, 'testdir'
assertEquals proj.issues.size(), 3
assertEquals proj.projects.size(), 2
// Issue construction in general is under test in IssueTest
// just check that the issues actually exists
assertEquals proj.issues['0001'].id, '0001'
assertEquals proj.issues['0001'].title, 'Test Issue 1'
assertEquals proj.issues['0002'].id, '0002'
assertEquals proj.issues['0002'].title, 'Test Bug'
assertEquals proj.issues['0003'].id, '0003'
assertEquals proj.issues['0003'].title, 'Important Feature Request'
// check sub-project behaviour
assertNotNull proj.projects.subproj1
assertEquals proj.projects.subproj1.name, 'subproj1'
assertEquals proj.projects.subproj1.issues.size(), 2
assertEquals proj.projects.subproj1.projects.size(), 0
assertEquals proj.projects.subproj1.issues['0001'].id, '0001'
assertEquals proj.projects.subproj1.issues['0002'].id, '0002'
assertEquals proj.projects.subproj1.issues['0001'].title,
'First feature in subproject'
assertEquals proj.projects.subproj1.issues['0002'].title,
'Zippners are not zippning.'
assertNotNull proj.projects.emptyproj
assertEquals proj.projects.emptyproj.issues.size(), 0
assertEquals proj.projects.emptyproj.projects.size(), 0
}
@Test void testRename() {
assert rootProj.name == 'testdir'
rootProj.name = 'renamedTestDir'
assertEquals rootProj.name, 'renamedTestDir'
assertTrue new File('renamedTestDir').exists()
assert rootProj.source.deleteDir()
}
@Test void testCreateNewIssue() {
// test correct increment of id, application of values
def newIssue = rootProj.createNewIssue(category: Category.BUG,
status: Status.REASSIGNED, priority: 4,
text: 'A newly made bug report.\n'+
'========================\n\n' +
'Testing the Project.createNewIssue() method.')
assertEquals newIssue.id, '0004'
assertEquals newIssue.category, Category.BUG
assertEquals newIssue.status, Status.REASSIGNED
assertEquals newIssue.priority, 4
assertEquals newIssue.text, 'A newly made bug report.\n'+
'========================\n\n' +
'Testing the Project.createNewIssue() method.'
assertEquals rootProj.issues[(newIssue.id)], newIssue
//test defaults and creation of issue in an empty project
newIssue = rootProj.projects.emptyproj.createNewIssue()
assertEquals newIssue.id, '0000'
assertEquals newIssue.priority, 5
assertEquals newIssue.category, Category.TASK
assertEquals newIssue.status, Status.NEW
assertEquals newIssue.text, 'Default issue title.\n' +
'====================\n'
assertEquals rootProj.projects.emptyproj.issues[(newIssue.id)],
newIssue
}
}

View File

@ -1,27 +0,0 @@
package com.jdbernard.pit.xml
import com.jdbernard.pit.*
import groovy.util.Node
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertFalse
import static org.junit.Assert.assertTrue
public class XmlIssueTest {
Node issueNode = new Node(null, 'Issue',
[id: '0000', category: 'BUG', status: 'RESOLVED', priority: 1],
'Test Issue')
@Test public void testDummyTest() {}
/*@Test public void testNodeConstructor() {
XmlIssue issue = new XmlIssue(issueNode)
assertEquals issue.text, 'Test Issue'
assertEquals issue.id, '0000'
assertEquals issue.category, Category.BUG
assertEquals issue.status, Status.RESOLVED
assertEquals issue.priority, 1
}*/
}

View File

@ -1,37 +0,0 @@
<project name="Personal Issue Tracker CLI">
<property file="project.properties"/>
<import file="../jdb-build-1.6.xml"/>
<target name="init">
<fail
unless="env.GROOVY_HOME"
message="GROOVY_HOME environment variable is not set."/>
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
<fail message="Could not find PIT ${version} library.">
<condition>
<not>
<available
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
</not>
</condition>
</fail>
</target>
<target name="lib">
<copy todir="${build.dir}/lib/compile/jar"
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
<copy todir="${build.dir}/lib/runtime/jar"
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
</target>
<target name="release" depends="build">
<mkdir dir="${release.dir}/lib"/>
<copy todir="${release.dir}/lib">
<fileset dir="${build.dir}/lib/runtime/jar"/></copy>
<copy tofile="${release.dir}/${name}-${version}.jar"
file="${build.dir}/${name}-${version}.${build.number}.jar"/>
</target>
</project>

View File

@ -1,12 +0,0 @@
#Thu, 08 Dec 2011 14:59:30 -0600
build.dir=build
src.dir=src
build.jar=pit-cli-${application.version}.${build.number}.jar
build.number=12
version=3.2.0
name=pit-cli
lib.dir=lib
lib.local=true
release.dir=release
release.jar=pit-cli-${application.version}.jar
main.class=com.jdbernard.pit.PersonalIssueTrackerCLI

View File

@ -1,439 +0,0 @@
package com.jdbernard.pit
import com.jdbernard.pit.file.*
import org.joda.time.DateMidnight
import org.joda.time.DateTime
import static java.lang.Math.max
import static java.lang.Math.min
// -------- command-line interface specification -------- //
def cli = new CliBuilder(usage: 'pit-cli [options]')
cli.h(longOpt: 'help', 'Show help information.')
cli.v(longOpt: 'verbose', 'Show verbose task information')
cli.l(longOpt: 'list', 'List issues. Unless otherwise specified it lists all '
+ 'sub projects and all unclosed issue categories.')
cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.')
cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task). Accepts a '
+ 'comma-delimited list. By default all categories are selected.')
cli.s(argName: 'status', longOpt: 'status', args: 1,
'Filter issues by status (new, reassigned, rejected, resolved, ' +
'validation_required)')
cli.p(argName: 'priority', longOpt: 'priority', args: 1,
'Filter issues by priority. This acts as a threshhold, listing all issues '
+ 'greater than or equal to the given priority.')
cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.')
cli.e(argName: 'extended-property', args: 1, 'Filter for issues by extended ' +
'property. Format is "-e <propname>=<propvalue>".')
/*cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)')
cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')*/ // TODO: figure out better flags for these options.
cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
'Modify the priority of the selected issues.')
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
'Modify the category of the selected issues.')
cli.S(argName: 'new-status', longOpt: 'set-status', args: 1,
'Modify the status of the selected issues.')
cli.E(argName: 'new-extended-property', args: 1, 'Modify the extended ' +
'property of the selected issues. Format is "-E <propname>=<propvalue>"')
cli.n(longOpt: 'new-issue', 'Create a new issue.')
cli._(longOpt: 'title', args: 1, argName: 'title', 'Give the title for a new' +
' issue or modify the title for an existing issue. By default the title' +
' for a new issue is expected on stanard input.')
cli._(longOpt: 'text', args: 1, argName: 'text', 'Give the text for a new' +
' issue or modify the text for an exising issue. By default the text for' +
' a new issue is expected on standard input.')
cli.o(longOpt: 'order', argName: 'order', args: 1, required: false,
'Order (sort) the results by the given properties. Provide a comma-' +
'seperated list of property names to sort by in order of importance. The' +
' basic properties (id, category, status, and priority) can be given' +
' using their one-letter forms (i,c,s,p) for brevity. For example:' +
' "-o Due,p,c" would sort first by the extended property "Due", then for' +
' items that have the same "Due" value it would sort by priority, then' +
' by category.')
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
'Use <dir> as the base directory (defaults to current directory).')
cli.D(longOpt: 'daily-list', 'Print a Daily Task list based on issue Due and' +
' Reminder properties.')
cli._(longOpt: 'dl-scheduled', 'Show scheduled tasks in the daily list (all' +
' are shown by default).')
cli._(longOpt: 'dl-due', 'Show due tasks in the daily list (all are shown by' +
' default).')
cli._(longOpt: 'dl-reminder', 'Show upcoming tasks in the daily list (all ' +
' are shown by default).')
cli._(longOpt: 'dl-open', 'Show open tasks in the daily list (all are shown ' +
' by default).')
cli._(longOpt: 'dl-hide-scheduled', 'Hide scheduled tasks in the daily list' +
' (all are shown by default).')
cli._(longOpt: 'dl-hide-due', 'Show due tasks in the daily list (all are' +
' shown by default).')
cli._(longOpt: 'dl-hide-reminder', 'Show upcoming tasks in the daily list' +
' (all are shown by default).')
cli._(longOpt: 'dl-hide-open', 'Show open tasks in the daily list (all are' +
' shown by default).')
cli._(longOpt: 'version', 'Display PIT version information.')
// =================================== //
// ======== Parse CLI Options ======== //
// =================================== //
def VERSION = "3.2.0"
def opts = cli.parse(args)
def issuedb = [:]
def workingDir = new File('.')
// defaults for the issue filter/selector
def selectOpts = [
categories: ['bug', 'feature', 'task'],
status: ['new', 'reassigned', 'rejected',
'resolved', 'validation_required'],
priority: 9,
projects: [],
ids: [],
extendedProperties: [:],
acceptProjects: true]
// options for changing properties of issue(s)
def assignOpts = [:]
if (!opts) opts.l = true; // default to 'list'
if (opts.h) {
cli.usage()
System.exit(0) }
// read the category filter designation(s)
if (opts.c) {
if (opts.c =~ /all/) {} // no-op, same as defaults
else { selectOpts.categories = opts.c.split(/[,\s]/) } }
// parse the categories names into Category objects
try { selectOpts.categories =
selectOpts.categories.collect { Category.toCategory(it) } }
catch (Exception e) {
println "Invalid category option: '-c ${e.localizedMessage}'."
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read the status filter designation(s)
if (opts.s) {
// -s all
if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected',
'resolved', 'validation_required']
// is <list>
else selectOpts.status = opts.s.split(/[,\s]/) }
// parse the statuses into Status objects
try { selectOpts.status =
selectOpts.status.collect { Status.toStatus(it) } }
catch (Exception e) {
println "Invalid status option: '-s ${e.localizedMessage}'."
print "Valid options are: \n${Status.values().join(', ')}"
println " (abbreviations are accepted.)"
System.exit(1) }
// read and parse the priority filter
if (opts.p) try {
selectOpts.priority = opts.p.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-p ${opts.p}'."
println "Valid values are: 0-9"
System.exit(1) }
// read and parse the projects filter
if (opts.r) { selectOpts.projects =
opts.r.toLowerCase().split(/[,\s]/).asType(List.class) }
// read and parse the ids filter
if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) }
// read and parse sort criteria
if (opts.o) {
def sortProps = opts.o.split(',')
selectOpts.issueSorter = sortProps.collect { prop ->
switch (prop) {
case ~/^i$/: return { issue -> issue.id }
case ~/^p$/: return { issue -> issue.priority }
case ~/^s$/: return { issue -> issue.status }
case ~/^c$/: return { issue -> issue.category }
default: return { issue -> issue[prop] } }}}
// read and parse extended property selection criteria
if (opts.e) {
opts.es.each { option ->
def parts = option.split("=")
selectOpts.extendedProperties[parts[0]] =
ExtendedPropertyHelp.parse(parts[1]) }}
// TODO: accept projects value from input
// read and parse the category to assign
if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) }
catch (Exception e) {
println "Invalid category option: '-C ${e.localizedMessage}'."
println "Valid categories are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the status to assign
if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) }
catch (Exception e) {
println "Invalid status option: '-S ${e.localizedMessage}'."
println "Valid stasus options are: \n{Status.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the priority to assign
if (opts.P) try {assignOpts.priority = opts.P.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-P ${opts.P}'."
println "Valid values are: 0-9"
System.exit(1) }
if (opts.E) {
opts.Es.each { option ->
def parts = option.split("=")
assignOpts[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }}
// Read the title if given.
if (opts.title) { assignOpts.title = opts.title }
// Read the text if given
if (opts.text) { assignOpts.text = opts.text }
// set the project working directory
if (opts.d) {
workingDir = new File(opts.d.trim())
if (!workingDir.exists()) {
println "Directory '${workingDir}' does not exist."
return -1 } }
def EOL = System.getProperty('line.separator')
// ========================= //
// ======== Actions ======== //
// ========================= //
// list version information first
if (opts.version) {
println "PIT CLI Version ${VERSION}"
println "Written by Jonathan Bernard\n" }
else {
// build issue list
issuedb = new FileProject(workingDir)
// build filter from options
def filter = new Filter(selectOpts)
// list second
if (opts.l) {
// local function (closure) to print a single issue
def printIssue = { issue, offset ->
println "${offset}${issue}"
if (opts.v) {
println ""
issue.text.eachLine { println "${offset} ${it}" }
issue.extendedProperties.each { name, value ->
def formattedValue = ExtendedPropertyHelp.format(value)
println "${offset} * ${name}: ${formattedValue}"}
println ""}}
// local function (closure) to print a project and all visible subprojects
def printProject
printProject = { project, offset ->
println "\n${offset}${project.name}"
println "${offset}${'-'.multiply(project.name.length())}"
project.eachIssue(filter) { printIssue(it, offset) }
project.eachProject(filter) { printProject(it, offset + " ") } }
// print all the issues in the root of this db
issuedb.eachIssue(filter) { printIssue(it, "") }
// print all projects
issuedb.eachProject(filter) { printProject(it, "") } }
// daily list second
else if (opts.D) {
// Parse daily list specific display options
def visibleSections = []
def suppressedSections
// Parse the additive options first.
if (opts.'dl-scheduled') { visibleSections << 'scheduled' }
if (opts.'dl-due') { visibleSections << 'due' }
if (opts.'dl-reminder') { visibleSections << 'reminder' }
if (opts.'dl-open') { visibleSections << 'open' }
// If the user did not add any sections assume they want them all.
if (visibleSections.size() == 0) {
visibleSections = ['scheduled', 'due', 'reminder', 'open'] }
// Now go through the negative options.
if (opts.'dl-hide-scheduled') { visibleSections -= 'scheduled' }
if (opts.'dl-hide-due') { visibleSections -= 'due' }
if (opts.'dl-hide-reminder') { visibleSections -= 'reminder' }
if (opts.'dl-hide-open') { visibleSections -= 'open' }
// If the user did not specifically ask for a status filter, we want a
// different filter for the default when we are doing a daily list.
if (!opts.s) { filter.status = [Status.NEW, Status.VALIDATION_REQUIRED] }
// If the user did not give a specific sorting order, define our own.
if (!opts.o) { filter.issueSorter = [ {it.due}, {it.priority}, {it.id} ] }
// Get our issues
def allIssues = issuedb.getAllIssues(filter)
// Set up our time interval.
def today = new DateMidnight()
def tomorrow = today.plusDays(1)
def scheduledToday = []
def dueToday = []
def reminderToday = []
def notDueOrReminder = []
def printIssue = { issue ->
if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}"
else println " -- ${issue}" }
// Sort the issues into seperate lists based on their due dates and
// reminders.
allIssues.each { issue ->
// Find the issues that are scheduled for today.
if (issue.scheduled && issue.scheduled < tomorrow) {
scheduledToday << issue }
// Find the issues that are due today or are past due.
else if (issue.due && issue.due < tomorrow) { dueToday << issue }
// Find the issues that are not yet due but have a reminder for today or
// days past.
else if (issue.reminder && issue.reminder < tomorrow) {
reminderToday << issue }
// All the others (not due and no reminder).
else notDueOrReminder << issue }
// Print the issues
if (visibleSections.contains('scheduled') && scheduledToday.size() > 0) {
println "Tasks Scheduled for Today"
println "-------------------------"
scheduledToday.each { printIssue(it) }
println "" }
if (visibleSections.contains('due') && dueToday.size() > 0) {
println "Tasks Due Today"
println "---------------"
dueToday.each { printIssue(it) }
println ""}
if (visibleSections.contains('reminder') && reminderToday.size() > 0) {
println "Upcoming Tasks"
println "--------------"
reminderToday.each { printIssue(it) }
println ""}
if (visibleSections.contains('open') && notDueOrReminder.size() > 0) {
println "Other Open Issues"
println "-----------------"
notDueOrReminder.each { printIssue(it) }
println "" }}
// new issues fourth
else if (opts.n) {
Issue issue
def sin = System.in.newReader()
// Set the created extended property
assignOpts.created = new DateTime()
// Prompt for the different options if they were not given on the command
// line. We will loop until they have entered a valid value. How it works:
// In the body of the loop we will try to read the input, parse it and
// assign it to a variable. If the input is invalid it will throw as
// exception before the assignment happens, the variable will still be
// null, and we will prompt the user again.
// Prompt for category.
while(!assignOpts.category) {
try {
print "Category (bug, feature, task): "
assignOpts.category = Category.toCategory(sin.readLine())
break }
catch (e) {
println "Invalid category: " + e.getLocalizedMessage()
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)." } }
// Prompt for the priority.
while (!assignOpts.priority) {
try {
print "Priority (0-9): "
assignOpts.priority = max(0, min(9, sin.readLine().toInteger()))
break }
catch (e) { println "Not a valid value." } }
// Prompt for the issue title. No need to loop as the input does not need
// to be validated.
if (!assignOpts.title) {
println "Issue title: "
assignOpts.title = sin.readLine().trim() }
// Prompt for the issue text.
if (!assignOpts.text) {
assignOpts.text = ""
println "Enter issue text (use EOF to stop): "
try {
def line = ""
while(true) {
line = sin.readLine()
// Stop when they enter EOF
if (line ==~ /^EOF$/) break
assignOpts.text += line + EOL } }
catch (e) {} }
issue = issuedb.createNewIssue(assignOpts)
println "New issue created: "
println issue }
// last, changes to existing issues
else if (assignOpts.size() > 0) {
// We are going to add some extra properties if the status is being changed,
// because we are nice like that.
if (assignOpts.status) { switch (assignOpts.status) {
case Status.RESOLVED: assignOpts.resolved = new DateTime(); break
case Status.REJECTED: assignOpts.rejected = new DateTime(); break
default: break }}
issuedb.walkProject(filter) { issue ->
println issue
assignOpts.each { propName, value ->
issue[propName] = value
println " set ${propName} to ${value}" } }}
else { cli.usage(); return -1 }}

View File

@ -1,6 +0,0 @@
#Griffon Metadata file
#Thu Aug 05 10:29:59 CDT 2010
app.archetype=default
app.griffon.version=0.9
app.name=pit-swing
app.version=2.5.1

View File

@ -1,33 +0,0 @@
application {
title = 'PitSwing'
startupGroups = ['PIT']
// Should Griffon exit when no Griffon created frames are showing?
autoShutdown = true
// If you want some non-standard application class, apply it here
//frameClass = 'javax.swing.JFrame'
}
mvcGroups {
// MVC Group for "ProjectPanel"
'ProjectPanel' {
model = 'com.jdbernard.pit.swing.ProjectPanelModel'
view = 'com.jdbernard.pit.swing.ProjectPanelView'
controller = 'com.jdbernard.pit.swing.ProjectPanelController'
}
// MVC Group for "NewIssueDialog"
'NewIssueDialog' {
model = 'com.jdbernard.pit.swing.NewIssueDialogModel'
view = 'com.jdbernard.pit.swing.NewIssueDialogView'
controller = 'com.jdbernard.pit.swing.NewIssueDialogController'
}
// MVC Group for "PIT"
'PIT' {
model = 'com.jdbernard.pit.swing.PITModel'
view = 'com.jdbernard.pit.swing.PITView'
controller = 'com.jdbernard.pit.swing.PITController'
}
}

View File

@ -1,135 +0,0 @@
// key signing information
environments {
development {
signingkey {
params {
sigfile = 'GRIFFON'
keystore = "${basedir}/griffon-app/conf/keys/devKeystore"
alias = 'development'
storepass = 'BadStorePassword'
keypass = 'BadKeyPassword'
lazy = true // only sign when unsigned
}
}
}
test {
griffon {
jars {
sign = false
pack = false
}
}
}
production {
signingkey {
params {
sigfile = 'GRIFFON'
keystore = 'CHANGE ME'
alias = 'CHANGE ME'
// NOTE: for production keys it is more secure to rely on key prompting
// no value means we will prompt //storepass = 'BadStorePassword'
// no value means we will prompt //keypass = 'BadKeyPassword'
lazy = false // sign, regardless of existing signatures
}
}
griffon {
jars {
sign = true
pack = true
destDir = "${basedir}/staging"
}
webstart {
codebase = 'CHANGE ME'
}
}
}
}
griffon {
memory {
//max = '64m'
//min = '2m'
//maxPermSize = '64m'
}
jars {
sign = false
pack = false
destDir = "${basedir}/staging"
jarName = "${appName}.jar"
}
extensions {
jarUrls = []
jnlpUrls = []
/*
props {
someProperty = 'someValue'
}
resources {
linux { // windows, macosx, solaris
jars = []
nativelibs = []
props {
someProperty = 'someValue'
}
}
}
*/
}
webstart {
codebase = "${new File(griffon.jars.destDir).toURI().toASCIIString()}"
jnlp = 'application.jnlp'
}
applet {
jnlp = 'applet.jnlp'
html = 'applet.html'
}
}
// required for custom environments
signingkey {
params {
def env = griffon.util.Environment.current.name
sigfile = 'GRIFFON-' + env
keystore = "${basedir}/griffon-app/conf/keys/${env}Keystore"
alias = env
// storepass = 'BadStorePassword'
// keypass = 'BadKeyPassword'
lazy = true // only sign when unsigned
}
}
griffon.project.dependency.resolution = {
// inherit Griffon' default dependencies
inherits("global") {
}
log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
repositories {
griffonPlugins()
griffonHome()
griffonCentral()
// uncomment the below to enable remote dependency resolution
// from public Maven repositories
//mavenLocal()
//mavenCentral()
//mavenRepo "http://snapshots.repository.codehaus.org"
//mavenRepo "http://repository.codehaus.org"
//mavenRepo "http://download.java.net/maven/2/"
//mavenRepo "http://repository.jboss.com/maven2/"
}
dependencies {
// specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
// runtime 'mysql:mysql-connector-java:5.1.5'
}
}
griffon {
doc {
logo = '<a href="http://griffon.codehaus.org" target="_blank"><img alt="The Griffon Framework" src="../img/griffon.png" border="0"/></a>'
sponsorLogo = "<br/>"
footer = "<br/><br/>Made with Griffon (0.9)"
}
}

View File

@ -1,9 +0,0 @@
root {
'groovy.swing.SwingBuilder' {
controller = ['Threading']
view = '*'
}
'griffon.app.ApplicationBuilder' {
view = '*'
}
}

View File

@ -1,19 +0,0 @@
// log4j configuration
log4j {
appender.stdout = 'org.apache.log4j.ConsoleAppender'
appender.'stdout.layout'='org.apache.log4j.PatternLayout'
appender.'stdout.layout.ConversionPattern'='[%r] %c{2} %m%n'
appender.errors = 'org.apache.log4j.FileAppender'
appender.'errors.layout'='org.apache.log4j.PatternLayout'
appender.'errors.layout.ConversionPattern'='[%r] %c{2} %m%n'
appender.'errors.File'='stacktrace.log'
rootLogger='error,stdout'
logger {
griffon='error'
StackTrace='error,errors'
org {
codehaus.griffon.commons='info' // core / classloading
}
}
additivity.StackTrace=false
}

View File

@ -1,5 +0,0 @@
import org.slf4j.LoggerFactory
onNewInstance = { klass, type, instance ->
instance.metaClass.logger = LoggerFactory.getLogger(klass.name)
}

View File

@ -1,44 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
</head>
<body>
<script src="http://java.com/js/deployJava.js"></script>
<script>
var attributes = {id: '@griffonAppName@',
codebase:'@griffonAppCodebase@',
code:'@griffonAppletClass@',
archive:'@appletJars@',
width:'@applet.width@', height:'@applet.height@'} ;
var parameters = {fontSize:16,
java_arguments: "-Djnlp.packEnabled=true",
jnlp_href:'@griffonAppCodebase@/applet.jnlp',
draggable:'true',
image:'griffon.png',
boxmessage:'Loading @griffonAppName@',
boxbgcolor:'#FFFFFF', boxfgcolor:'#000000',
codebase_lookup: 'false'@applet.script.params@} ;
var version = '1.5.0' ;
deployJava.runApplet(attributes, parameters, version);
</script>
<!--
<APPLET CODEBASE='@griffonAppCodebase@'
CODE='@griffonAppletClass@'
ARCHIVE='@appletJars@'
WIDTH='@applet.width@' HEIGHT='@applet.height@'>
<PARAM NAME="java_arguments" VALUE="-Djnlp.packEnabled=true">
<PARAM NAME='jnlp_href' VALUE='@griffonAppCodebase@/applet.jnlp'>
<PARAM NAME='dragggable' VALUE='true'>
<PARAM NAME='image' VALUE='griffon.png'>
<PARAM NAME='boxmessage' VALUE='Loading @griffonAppName@'>
<PARAM NAME='boxbgcolor' VALUE='#FFFFFF'>
<PARAM NAME='boxfgcolor' VALUE='#000000'>
<PARAM NAME='codebase_lookup' VALUE='false'>
@applet.tag.params@
</APPLET>
-->
</body>
</html>

View File

@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE jnlp SYSTEM "http://java.sun.com/dtd/JNLP-1.5.dtd">
<jnlp
version="@griffonAppVersion@"
codebase="@griffonAppCodebase@"
href="@jnlpFileName@"
>
<information>
<title>@griffonAppName@</title>
<vendor>@griffonAppName@</vendor>
<!--<homepage href="http://app.example.com/"/>-->
<!--fallback description-->
<description>@griffonAppName@</description>
<description kind="one-line">@griffonAppName@</description>
<description kind="short">@griffonAppName@</description>
<description kind="tooltip">@griffonAppName@</description>
<!-- fallback icon -->
<icon href="griffon-icon-48x48.png" kind="default" width="48" height="48"/>
<!-- icon used for splash screen -->
<icon href="griffon.png" kind="splash" width="381" height="123"/>
<!-- icon used in menu -->
<icon href="griffon-icon-16x16.png" kind="shortcut" width="16" height="16"/>
<!-- icon used on desktop -->
<icon href="griffon-icon-32x32.png" kind="shortcut" width="32" height="32"/>
<!-- to create shortcuts, uncomment this
<shortcut online="true">
<desktop/>
<menu submenu="@griffonAppName@"/>
</shortcut>
-->
<offline-allowed/>
</information>
<security>
<all-permissions/>
<!--<j2ee-application-client-permissions/>-->
</security>
<resources>
<property name="griffon.runmode" value="applet"/>
<property name="jnlp.packEnabled" value="true"/>
<j2se version="1.5+" @memoryOptions@/>
<!-- auto-added jars follow, griffon-rt, app, and groovy -->
@jnlpJars@
<!-- Add all extra jars below here, or the app may break -->
@jnlpExtensions@
@jnlpProperties@
</resources>
@jnlpResources@
<applet-desc
documentbase="@griffonAppCodebase@"
name="@griffonAppName@Applet"
main-class="@griffonAppletClass@"
width="@applet.width@"
height="@applet.height@">
<!-- params are ignored when referenced from web page for 6u10 -->
<!--<param name="key1" value="value1"/>-->
<!--<param name="key2" value="value2"/>-->
@applet.tag.params@
</applet-desc>
</jnlp>

View File

@ -1,54 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE jnlp SYSTEM "http://java.sun.com/dtd/JNLP-1.5.dtd">
<jnlp
version="@griffonAppVersion@"
codebase="@griffonAppCodebase@"
href="@jnlpFileName@"
>
<information>
<title>@griffonAppName@</title>
<vendor>@griffonAppName@</vendor>
<!--<homepage href="http://app.example.com/"/>-->
<!--fallback description-->
<description>@griffonAppName@</description>
<description kind="one-line">@griffonAppName@</description>
<description kind="short">@griffonAppName@</description>
<description kind="tooltip">@griffonAppName@</description>
<!-- fallback icon -->
<icon href="griffon-icon-48x48.png" kind="default" width="48" height="48"/>
<!-- icon used for splash screen -->
<icon href="griffon.png" kind="splash" width="381" height="123"/>
<!-- icon used in menu -->
<icon href="griffon-icon-16x16.png" kind="shortcut" width="16" height="16"/>
<!-- icon used on desktop -->
<icon href="griffon-icon-32x32.png" kind="shortcut" width="32" height="32"/>
<!-- to create shortcuts, uncomment this
<shortcut online="true">
<desktop/>
<menu submenu="@griffonAppName@"/>
</shortcut>
<offline-allowed/>
-->
</information>
<security>
<all-permissions/>
<!--<j2ee-application-client-permissions/>-->
</security>
<resources>
<property name="griffon.runmode" value="webstart"/>
<property name="jnlp.packEnabled" value="true"/>
<j2se version="1.5+" @memoryOptions@/>
<!-- auto-added jars follow, griffon-rt, app, and groovy -->
@jnlpJars@
<!-- Add all extra jars below here, or the app may break -->
@jnlpExtensions@
@jnlpProperties@
</resources>
@jnlpResources@
<application-desc main-class="@griffonApplicationClass@">
<!-- params are ignored when referenced from web page for 6u10 -->
<!--<param name="key1" value="value1"/>-->
<!--<param name="key2" value="value2"/>-->
@applet.tag.params@
</application-desc>
</jnlp>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,26 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Status
class NewIssueDialogController {
// these will be injected by Griffon
def model
def view
void mvcGroupInit(Map args) {
// this method is called after model and view are injected
}
def show = {
view.titleTextField.text = ""
model.text = ""
view.categoryComboBox.selectedItem = Category.BUG
model.category = Category.BUG
view.statusComboBox.selectedItem = Status.NEW
model.status = Status.NEW
view.prioritySpinner.setValue(5)
model.priority = 5
view.dialog.visible = true
}
}

View File

@ -1,172 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.FileProject
import javax.swing.JFileChooser
import javax.swing.SwingUtilities
class PITController {
// these will be injected by Griffon
def model
def view
void mvcGroupInit(Map args) {
model.newIssueDialogMVC = buildMVCGroup('NewIssueDialog')
SwingUtilities.invokeAndWait {
model.issueListRenderer = new IssueTableCellRenderer()
File pitHome, pitrcFile, pitswingrcFile
boolean logDbg = logger.isDebugEnabled()
Properties config = new Properties()
// look for config directory
pitHome = new File(System.getProperty('user.home'), '.pit')
if (logDbg) logger.debug("$pitHome is " +
(pitHome.exists() ? '' : 'not ') + "present.")
// look for general config options
pitrcFile = new File(pitHome, 'pitrc')
if (logDbg) logger.debug("$pitrcFile is " +
(pitrcFile.exists() ? '' : 'not ') + "present.")
// load general config (if present)
if (pitrcFile.exists() && pitrcFile.canRead()) {
pitrcFile.withInputStream { config.load(it) }
if (logDbg) logger.debug("Loaded pitrc")
}
// look for swing specific config
pitswingrcFile = new File(pitHome, 'pitswingrc')
if (logDbg) logger.debug("$pitswingrcFile is " +
(pitswingrcFile.exists() ? '' : 'not ') + "present.")
// load swing specific config (if present)
if (pitswingrcFile.exists() && pitswingrcFile.canRead()) {
pitswingrcFile.withInputStream { config.load(it) }
if (logDbg) logger.debug("Loaded pitswingrc")
}
// Process configurable options
// ----------------------------
if (logDbg) {
logger.debug("Configurable properties:")
config.keySet().each { logger.debug(it) }
}
// add custom category templates
Category.values().each { category ->
def expectedKey = "issue." + category.name().toLowerCase() +
".template"
if (logDbg) logger.debug("Looking for key: $expectedKey")
config.keySet().each { currentKey ->
if (currentKey == expectedKey)
model.templates[(category)] =
config.getProperty(expectedKey, "")
if (logDbg) logger.debug("Template for category $category: '" +
model.templates[(category)] + "'")
}
}
// load custom issueListRenderer
// TODO: not yet supported (maybe no need)
// load initial repositories
if (config.containsKey('initial-repositories')) {
def initRepos = config.getProperty('initial-repositories', '')
initRepos = initRepos.split(/[:;,]/)
initRepos.each { repoPath -> loadProject(new File(repoPath)) }
if (logDbg) logger.debug("Init repos: '$initRepos'")
}
// load custom issue css
if (config.containsKey('issue.display.css')) {
def issueCSS = config.getProperty('issue.display.css', "")
// look for a file relative to the pit home directory
def cssFile
// use short-circuit logic to test several possible locations
if ((cssFile = new File(pitHome, issueCSS)).exists() ||
(cssFile = new File(pitHome.parentFile(), issueCSS)).exists() ||
(cssFile = new File(issueCSS)).exists())
issueCSS = cssFile.text
if (logDbg) logger.debug("CSS for issue display: $issueCSS")
model.issueCSS = issueCSS
}
}
}
void refreshIssues() {
model.projectPanelMVCs.each { title, mvc ->
mvc.controller.refreshIssues()
}
}
def openProject = { evt = null ->
if (view.openDialog.showOpenDialog(view.frame) !=
JFileChooser.APPROVE_OPTIONS) return
loadProject(view.openDialog.selectedFile)
}
def loadProject = { File projectDir ->
def newMVC
// if this is not a valid directory, do nothing
// TODO: log to the user that this is not a valid directory
if (!projectDir.exists() || !projectDir.isDirectory()) return
// create new ProjectPanel MVC
newMVC = buildMVCGroup('ProjectPanel',
mainMVC: [model: model, view: view, controller: this],
newIssueDialogMVC: model.newIssueDialogMVC,
issueCellRenderer: model.issueListRenderer,
issueCSS: model.issueCSS,
rootProject: new FileProject(projectDir))
newMVC.model.id = projectDir.name
// if we already have a tab with this id
if (model.projectPanelMVCs[(newMVC.model.id)]) {
// try using the canonical path
newMVC.model.id = projectDir.canonicalPath
// still not unique?
if (model.projectPanelMVCs[(newMVC.model.id)]) {
// first time this has happened?
if (!model.projectIdMap[(newMVC.model.id)])
model.projectIdMap[(newMVC.model.id)] = 0
// no? increment
else model.projectIdMap[(newMVC.model.id)] =
model.projectIdMap[(newMVC.model.id)] + 1
// use our new, unique id
newMVC.model.id += "-" + model.projectIdMap[(newMVC.model.id)]
}
}
model.projectPanelMVCs[(newMVC.model.id)] = newMVC
view.mainTabbedPane.addTab(newMVC.model.id, newMVC.view.panel)
}
def closeProject = { evt = null ->
model.projectPanelMVCs.remove(view.mainTabbedPane.getTitleAt(
view.mainTabbedPane.selectedIndex))
view.mainTabbedPane.remove(view.mainTabbedPane.selectedComponent)
}
def shutdown = { evt = null ->
app.shutdown()
}
}

View File

@ -1,222 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.FileProject
import com.jdbernard.pit.FlatProjectView
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import com.jdbernard.pit.Status
import javax.swing.DefaultListModel
import javax.swing.JOptionPane
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import org.dom4j.Document
import org.dom4j.io.OutputFormat
import org.dom4j.io.XMLWriter
import org.nuiton.jrst.JRSTGenerator
import org.nuiton.jrst.JRSTReader
class ProjectPanelController {
// these will be injected by Griffon
def model
def view
def jrstReader
def jrstGen
static URL rst2htmlXSL =
ProjectPanelController.class.getResource("/rst2xhtml.xsl")
void mvcGroupInit(Map args) {
jrstReader = new JRSTReader()
jrstGen = new JRSTGenerator()
refreshProject()
}
/**
* displayProject
* @param project Project to display
*/
void displayProject(Project project) {
if (!project) return
view.issueTextArea.text = ""
view.issueTextDisplay.text = ""
view.issueTextPanelLayout.show(view.issueTextPanel, 'display')
// build a new IssueTableModel if none cached
if (!model.projectTableModels[(project.name)]) {
def itm = new IssueTableModel(project,
model.filter ?: model.mainMVC.model.filter)
itm.categoryIcons = model.mainMVC.model.categoryIcons
itm.statusIcons = model.mainMVC.model.statusIcons
model.projectTableModels[(project.name)] = itm
}
view.issueTable.setModel(model.projectTableModels[(project.name)])
def tcm = view.issueTable.columnModel
tcm.getColumn(0).maxWidth = 24
tcm.getColumn(1).maxWidth = 40
tcm.getColumn(2).maxWidth = 35
if (view.issueTable.model.columnCount == 5)
tcm.getColumn(4).maxWidth = 150
}
void displayIssue(Issue issue) {
if (!issue) return
// hack because binding view.issueTextArea.font to
// mainMVC.model.issueDetailFont causes problems
if (view.issueTextArea.font != model.mainMVC.model.issueDetailFont)
view.issueTextArea.font = model.mainMVC.model.issueDetailFont
view.issueTextArea.text = issue.text
view.issueTextArea.caretPosition = 0
view.issueTextDisplay.text = rst2html(issue.text)
view.issueTextDisplay.caretPosition = 0
view.issueTextPanelLayout.show(view.issueTextPanel, 'display')
}
void showProejctPopup(Project project, def x, def y) {
model.popupProject = project
view.projectPopupMenu.show(view.projectTree, x, y)
}
void showIssuePopup(Issue issue, def x, def y) {
model.popupIssue = issue
view.issuePopupMenu.show(view.issueTable, x, y)
}
void refreshProject() {
if (model.rootProject) {
def rootNode = new DefaultMutableTreeNode()
def flatview = new FlatProjectView('All Issues')
flatview.projects[(model.rootProject.name)] = model.rootProject
rootNode.add(new DefaultMutableTreeNode(flatview))
rootNode.add(makeNodes(model.rootProject))
view.projectTree.model = new DefaultTreeModel(rootNode)
} else {
view.projectTree.model = new DefaultTreeModel(
new DefaultMutableTreeNode())
}
}
void refreshIssues() {
model.projectTableModels.clear()
displayProject(model.selectedProject)
}
def makeNodes(Project project) {
def rootNode = new DefaultMutableTreeNode(project)
project.eachProject(model.filter ?: model.mainMVC.model.filter)
{ rootNode.add(makeNodes(it)) }
return rootNode
}
def newProject = { evt ->
def name = JOptionPane.showInputDialog(model.mainMVC.view.frame,
'Project name:', 'New Project...', JOptionPane.QUESTION_MESSAGE)
def project
if (evt.source == view.newProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootProject
def newProject = project.createNewProject(name)
project.projects[(newProject.name)] = newProject
refreshProject()
}
def deleteProject = { evt ->
def project
if (evt.source == view.deleteProjectButton)
project = model.selectedProject ?: model.rootProject
else project = model.popupProject ?: model.rootProject
project.delete()
model.rootProject = new FileProject(model.rootProject.source)
}
def newIssue = { evt = null ->
model.newIssueDialogMVC.controller.show()
if (model.newIssueDialogMVC.model.accept) {
def nidModel = model.newIssueDialogMVC.model
def issueText = nidModel.text
if (model.mainMVC.model.templates[(nidModel.category)]) {
issueText = model.mainMVC.model.templates[(nidModel.category)]
issueText = issueText.replaceFirst(/TITLE/,
nidModel.text)
}
def issue = model.selectedProject.createNewIssue(
category: nidModel.category,
status: nidModel.status,
priority: nidModel.priority,
text: issueText)
model.projectTableModels[(model.selectedProject.name)] = null
displayProject(model.selectedProject)
}
}
def deleteIssue = { evt ->
def issue
if (evt.source == view.deleteIssueButton)
issue = getSelectedIssue()
else issue = model.popupIssue
model.selectedProject.issues.remove(issue.id)
view.issueTable.model.issues.remove(issue)
issue.delete()
view.issueTable.invalidate()
}
def getSelectedIssue() {
if (view.issueTable.selectionModel.isSelectionEmpty())
return null
return view.issueTable.model.issues[view.issueTable.
convertRowIndexToModel(view.issueTable.selectedRow)]
}
String rst2html(String rst) {
Document doc
StringWriter outString
StringBuilder result = new StringBuilder()
// read the RST in with the RST parser
new StringReader(rst).withReader { doc = jrstReader.read(it) }
// transform to XHTML
doc = jrstGen.transform(doc, rst2htmlXSL)
// write to the StringWriter
outString = new StringWriter()
outString.withWriter { new XMLWriter(it, new OutputFormat("", true)).write(doc) }
// java's embeded html is primitive, we need to massage the results
outString.toString().eachLine { line ->
// remove the XML version and encoding, title element, meta elems
if (line =~ /<\?.*\?>/ || line =~ /<meta.*$/ || line =~ /<title.*$/) { return }
// all other elements, remove all class, xmlns attributes
def m = (line =~ /(<\S+)(\s*(class|xmlns)=".*"\s*)*(\/?>.*)/)
if (m) line = m[0][1] + m[0][4]
result.append(line)
// add in the CSS information to the head
if (line =~ /<head>/) result.append('<style type="text/css">' +
model.issueCSS + '</style>')
}
return result.toString()
}
}

View File

@ -1,24 +0,0 @@
/*
* This script is executed inside the UI thread, so be sure to call
* long running code in another thread.
*
* You have the following options
* - execOutside { // your code }
* - execFuture { // your code }
* - Thread.start { // your code }
*
* You have the following options to run code again inside the UI thread
* - execAsync { // your code }
* - execSync { // your code }
*/
import groovy.swing.SwingBuilder
import griffon.util.GriffonPlatformHelper
import static griffon.util.GriffonApplicationUtils.*
GriffonPlatformHelper.tweakForNativePlatform(app)
SwingBuilder.lookAndFeel('org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel', 'nimbus', ['metal', [boldFonts: false]])
// make config directory
def confDir = new File(System.getProperty('user.home'), '.pit')
if (!confDir.exists()) confDir.mkdirs()

View File

@ -1,13 +0,0 @@
/*
* This script is executed inside the UI thread, so be sure to call
* long running code in another thread.
*
* You have the following options
* - execOutside { // your code }
* - execFuture { // your code }
* - Thread.start { // your code }
*
* You have the following options to run code again inside the UI thread
* - execAsync { // your code }
* - execSync { // your code }
*/

View File

@ -1,13 +0,0 @@
/*
* This script is executed inside the UI thread, so be sure to call
* long running code in another thread.
*
* You have the following options
* - execOutside { // your code }
* - execFuture { // your code }
* - Thread.start { // your code }
*
* You have the following options to run code again inside the UI thread
* - execAsync { // your code }
* - execSync { // your code }
*/

View File

@ -1,13 +0,0 @@
/*
* This script is executed inside the UI thread, so be sure to call
* long running code in another thread.
*
* You have the following options
* - execOutside { // your code }
* - execFuture { // your code }
* - Thread.start { // your code }
*
* You have the following options to run code again inside the UI thread
* - execAsync { // your code }
* - execSync { // your code }
*/

View File

@ -1,13 +0,0 @@
/*
* This script is executed inside the UI thread, so be sure to call
* long running code in another thread.
*
* You have the following options
* - execOutside { // your code }
* - execFuture { // your code }
* - Thread.start { // your code }
*
* You have the following options to run code again inside the UI thread
* - execAsync { // your code }
* - execSync { // your code }
*/

View File

@ -1,13 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Status
import groovy.beans.Bindable
class NewIssueDialogModel {
@Bindable boolean accept
String text
Category category
Status status
int priority
}

View File

@ -1,34 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Category
import com.jdbernard.pit.Filter
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import com.jdbernard.pit.Status
import groovy.beans.Bindable
import java.awt.Font
import javax.swing.ImageIcon
class PITModel {
// filter for projects and classes
Filter filter = new Filter(categories: [],
status: [Status.NEW, Status.VALIDATION_REQUIRED])
def issueListRenderer
// map of category -> issue template
Map<Category, String> templates = [:]
String issueCSS = getClass().getResourceAsStream("/default-issue.css").text
Map<Category, ImageIcon> categoryIcons = [:]
Map<Category, ImageIcon> statusIcons = [:]
def newIssueDialogMVC
Map projectPanelMVCs = [:]
Map projectIdMap = [:]
@Bindable Font issueDetailFont = new Font(Font.MONOSPACED, Font.PLAIN, 10)
}

View File

@ -1,29 +0,0 @@
package com.jdbernard.pit.swing
import com.jdbernard.pit.Filter
import com.jdbernard.pit.Issue
import com.jdbernard.pit.Project
import groovy.beans.Bindable
class ProjectPanelModel {
// other GUI components
def mainMVC
def newIssueDialogMVC
// data owned by this panel
String id
@Bindable Project rootProject
@Bindable Project popupProject = null
@Bindable Project selectedProject = null
@Bindable Issue popupIssue = null
String issueCSS = ""
// cache the models
def projectTableModels = [:]
def issueCellRenderer
// local filter for projects and issues
Filter filter
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 587 B

View File

@ -1,22 +0,0 @@
body {
font-size: small;
}
h1 {
font-size: medium;
text-decoration: underline;
}
h2 {
font-size: small;
}
h3 {
font-size: small;
font-style: italic;
}
h4 {
font-size: small;
font-weight: normal;
font-style: italic;
}
table,th,td{
border-style: solid;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 537 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

View File

@ -1,10 +0,0 @@
log4j.rootLogger=DEBUG,stdout,fileout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout
log4j.appender.fileout=org.apache.log4j.FileAppender
log4j.appender.fileout.file=pit-swing.log
log4j.appender.fileout.layout=org.apache.log4j.PatternLayout
log4j.appender.fileout.layout.ConversionPattern=%-5p %C %d{DATE}: %m%n
log4j.appender.fileout.threshold=INFO

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 B

View File

@ -1,495 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/TR/xhtml1/strict">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:template match="/document">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="generator" content="JRST http://maven-site.nuiton.org/jrst" />
<title><xsl:value-of select="title"/></title>
</head>
<body>
<xsl:apply-templates/>
</body>
</html>
</xsl:template>
<xsl:template match="comment">
<xsl:comment>
<xsl:text> </xsl:text>
<xsl:apply-templates/>
<xsl:text> </xsl:text>
</xsl:comment>
</xsl:template>
<xsl:template match="title">
<xsl:if test="name(..)='document'">
<h1 class="mainTitle">
<xsl:apply-templates/>
</h1>
</xsl:if>
<xsl:if test="not(name(..)='document')">
<xsl:element name="h{count(ancestor::section) + 1}">
<xsl:attribute name="class">title</xsl:attribute>
<xsl:if test="@refid">
<a class="toc-backref" href="#{@refid}" id="{../@id}"><xsl:apply-templates/></a>
</xsl:if>
<xsl:if test="not(@refid)">
<xsl:apply-templates/>
</xsl:if>
</xsl:element>
</xsl:if>
</xsl:template>
<xsl:template match="subtitle">
<xsl:element name="h2">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<!-- just eat it -->
<xsl:template match="substitution_definition">
</xsl:template>
<xsl:template match="docinfo">
<table class="docinfo" frame="void" rules="none">
<col class="docinfo-name" />
<col class="docinfo-content" />
<tbody valign="top">
<xsl:apply-templates/>
</tbody>
</table>
</xsl:template>
<xsl:template match="organization|address|contact|version|revision|status|date|copyright">
<tr>
<th class="docinfo-name">
<xsl:value-of select="name(.)"/> :
</th>
<td class="docinfo-content">
<xsl:apply-templates/>
</td>
</tr>
</xsl:template>
<xsl:template match="author">
<xsl:if test="not(../../authors)">
<tr>
<th class="docinfo-name">
<xsl:value-of select="name(.)"/> :
</th>
<td class="docinfo-content">
<xsl:apply-templates/>
</td>
</tr>
</xsl:if>
<xsl:if test="../../authors">
<xsl:variable name="num" select="position()"/>
<xsl:if test="$num=1">
<tr>
<th class="docinfo-name">
<xsl:value-of select="authors"/>authors :
</th>
<td class="docinfo-content">
<xsl:apply-templates/>
</td>
</tr>
</xsl:if>
<xsl:if test="$num>1">
<tr>
<th>
</th>
<td class="docinfo-content">
<xsl:apply-templates/>
</td>
</tr>
</xsl:if>
</xsl:if>
</xsl:template>
<xsl:template match="transition">
<hr/>
</xsl:template>
<xsl:template match="section">
<a name="{@id}"></a>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="list_item/paragraph[1] | definition_list_item/*/paragraph[1] | field/*/paragraph[1] | option/*/paragraph[1]">
<!--XXX - Unclear how to handle multi-paragraph list items.
| Certainly when they're single paragraphs, we don't want them
| wrapped in a <P> tag. This seems to work okay.
+-->
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="paragraph">
<p><xsl:apply-templates/></p>
</xsl:template>
<xsl:template match="reference">
<xsl:if test="@refid">
<a href="{@refuri}#{@refid}" id="{@id}"><xsl:apply-templates/></a>
</xsl:if>
<xsl:if test="not(@refid)">
<a href="{@refuri}" id="{@id}"><xsl:apply-templates/></a>
</xsl:if>
</xsl:template>
<xsl:template match="emphasis">
<em><xsl:apply-templates/></em>
</xsl:template>
<xsl:template match="strong">
<b><xsl:apply-templates/></b>
</xsl:template>
<xsl:template match="literal">
<code><xsl:value-of select="text()"/></code>
</xsl:template>
<xsl:template match="literal_block">
<pre class="literal_block"><xsl:value-of select="text()"/></pre>
</xsl:template>
<xsl:template match="bullet_list">
<ul><xsl:apply-templates/></ul>
</xsl:template>
<xsl:template match="enumerated_list">
<ol>
<xsl:choose>
<xsl:when test="@enumtype='arabic'">
<xsl:attribute name="type">1</xsl:attribute>
</xsl:when>
<xsl:when test="@enumtype='loweralpha'">
<xsl:attribute name="type">a</xsl:attribute>
</xsl:when>
<xsl:when test="@enumtype='upperalpha'">
<xsl:attribute name="type">A</xsl:attribute>
</xsl:when>
<xsl:when test="@enumtype='lowerroman'">
<xsl:attribute name="type">i</xsl:attribute>
</xsl:when>
<xsl:when test="@enumtype='upperroman'">
<xsl:attribute name="type">I</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:copy-of select="@start"/>
<xsl:apply-templates/>
</ol>
</xsl:template>
<xsl:template match="list_item">
<li><xsl:apply-templates/></li>
</xsl:template>
<xsl:template match="field_list">
<div class="field_list"><xsl:apply-templates/></div>
</xsl:template>
<xsl:template match="field">
<xsl:if test="not(../../docinfo)">
<div class="field"><xsl:apply-templates/></div>
</xsl:if>
<xsl:if test="../../docinfo">
<tr>
<th class="docinfo-name">
<xsl:value-of select="field_name/text()"/> :
</th>
<td>
<xsl:apply-templates select="field_body/*"/>
</td>
</tr>
</xsl:if>
</xsl:template>
<xsl:template match="field_name">
<span class="field_name"><xsl:apply-templates/></span>
</xsl:template>
<xsl:template match="field_body">
<span class="field_body"><xsl:apply-templates/></span>
</xsl:template>
<xsl:template match="definition_list">
<dl class="definition_list"><xsl:apply-templates/></dl>
</xsl:template>
<xsl:template match="definition_list_item">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="term">
<dt class="term"><xsl:apply-templates/><xsl:call-template name="classifier"/></dt>
</xsl:template>
<xsl:template name="classifier">
<xsl:for-each select="../classifier">
<span class="classifier"><xsl:apply-templates/></span>
</xsl:for-each>
</xsl:template>
<xsl:template match="classifier">
<!-- do nothing -->
</xsl:template>
<xsl:template match="definition">
<dd class="definition"><xsl:apply-templates/></dd>
</xsl:template>
<xsl:template match="image">
<xsl:choose>
<xsl:when test="(@target) and (@align)">
<div class="align-{@align}" align="{@align}">
<a href="{@target}">
<xsl:call-template name="img" />
</a>
</div>
</xsl:when>
<xsl:when test="@target">
<a href="{@target}">
<xsl:call-template name="img" />
</a>
</xsl:when>
<xsl:when test="@align">
<div class="align-{@align}" align="{@align}">
<xsl:call-template name="img" />
</div>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="img" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="img">
<xsl:element name="img">
<xsl:attribute name="alt"><xsl:value-of select="@alt"/></xsl:attribute>
<xsl:attribute name="src"><xsl:value-of select="@uri"/></xsl:attribute>
<xsl:if test="@width"><xsl:attribute name="width"><xsl:value-of select="@width"/></xsl:attribute></xsl:if>
<xsl:if test="@height"><xsl:attribute name="height"><xsl:value-of select="@height"/></xsl:attribute></xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<xsl:template match="footer">
<hr/>
<p class="footer"><xsl:apply-templates/></p>
</xsl:template>
<xsl:template match="header">
<p class="header"><xsl:apply-templates/></p>
<hr/>
</xsl:template>
<!--
| Table
+-->
<xsl:template match="table">
<table border="1">
<colgroup>
<xsl:apply-templates select="tgroup/colspec"/>
</colgroup>
<xsl:apply-templates select="./tgroup/thead|./tgroup/tbody"/>
</table>
</xsl:template>
<xsl:template match="tgroup/colspec">
<col width="{@colwidth}%"/>
</xsl:template>
<xsl:template match="row">
<tr><xsl:apply-templates/></tr>
</xsl:template>
<xsl:template match="thead">
<thead><xsl:apply-templates/></thead>
</xsl:template>
<xsl:template match="thead/row/entry">
<th>
<xsl:if test="@morecols"><xsl:attribute name="colspan"><xsl:value-of select="@morecols+1"/></xsl:attribute></xsl:if>
<xsl:if test="@morerows"><xsl:attribute name="rowspan"><xsl:value-of select="@morerows+1"/></xsl:attribute></xsl:if>
<xsl:apply-templates/>
</th>
</xsl:template>
<xsl:template match="tbody">
<tbody><xsl:apply-templates/></tbody>
</xsl:template>
<xsl:template match="tbody/row/entry">
<td>
<xsl:if test="@morecols"><xsl:attribute name="colspan"><xsl:value-of select="@morecols+1"/></xsl:attribute></xsl:if>
<xsl:if test="@morerows"><xsl:attribute name="rowspan"><xsl:value-of select="@morerows+1"/></xsl:attribute></xsl:if>
<xsl:apply-templates/>
</td>
</xsl:template>
<xsl:template match="admonition">
<div class="admonition">
<div class="{@class}">
<p class="{title}">
<xsl:apply-templates select="./title"/>
</p>
<p class="body">
<xsl:apply-templates select="child::*[position()>1]"/>
</p>
</div>
</div>
</xsl:template>
<xsl:template match="attention|caution|danger|error|hint|important|note|tip|warning">
<div class="{name(.)}">
<p class="title"><xsl:value-of select="name(.)"/> :</p>
<p class="body">
<xsl:apply-templates/>
</p>
</div>
</xsl:template>
<xsl:template match="block_quote">
<blockquote>
<xsl:if test="./attribution">
<p><xsl:apply-templates select="child::*[position()=1]"/></p>
<p class="attribution">
<xsl:apply-templates select="./attribution"/>
</p>
</xsl:if>
<xsl:if test="not(./attribution)">
<xsl:apply-templates select="child::*"/>
</xsl:if>
</blockquote>
</xsl:template>
<xsl:template match="doctest_block">
<pre class="doctest_block">
<xsl:apply-templates/>
</pre>
</xsl:template>
<xsl:template match="line_block">
<div class="line_block">
<xsl:apply-templates/>
</div>
</xsl:template>
<xsl:template match="line">
<div class="line">
<xsl:apply-templates/>
</div>
</xsl:template>
<xsl:template match="sidebar">
<div class="sidebar">
<p class="title">
<xsl:apply-templates select="./title"/>
</p>
<xsl:if test="./subtitle">
<p class="subtitle">
<xsl:apply-templates select="./subtitle"/>
</p>
</xsl:if>
<xsl:choose>
<xsl:when test="./subtitle">
<xsl:apply-templates select="child::*[position()>2]"/>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="child::*[position()>1]"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:template>
<xsl:template match="topic">
<div class="topic">
<p class="title">
<xsl:apply-templates select="./title"/>
</p>
<xsl:apply-templates select="child::*[position()>1]"/>
</div>
</xsl:template>
<xsl:template match="option_list">
<table class="option_list">
<col class="option" />
<col class="description" />
<tbody valign="top">
<xsl:apply-templates/>
</tbody>
</table>
</xsl:template>
<xsl:template match="option_list_item">
<tr>
<td class="option-group">
<kbd>
<xsl:apply-templates select="./option_group/option"/>
</kbd>
</td>
<td>
<xsl:apply-templates select="./description"/>
</td>
</tr>
</xsl:template>
<xsl:template match="option">
<span class="option">
<xsl:value-of select="option_string/text()"/>
<xsl:value-of select="./option_argument/@delimiter"/>
<xsl:apply-templates select="./option_argument"/>
</span>
</xsl:template>
<xsl:template match="option_argument">
<var>
<xsl:value-of select="text()"/>,
</var>
</xsl:template>
<xsl:template match="footnote">
<table class="footnote" frame="void" id="{@id}" rules="none">
<colgroup>
<col class="label"/>
<col/>
</colgroup>
<tbody valign="top">
<tr>
<td class="label">
<a class="backref" href="#{@backrefs}" name="{id}">
[<xsl:value-of select="label"/>]
</a>
</td>
<td>
<!--
| <xsl:value-of select="child::*[position()>1]"/>
+-->
<xsl:apply-templates select="child::*[position()>1]"/>
</td>
</tr>
</tbody>
</table>
</xsl:template>
<xsl:template match="footnote_reference">
<a class="footnote_reference" href="#{@refid}" id="{@id}" name="{@id}">
[<xsl:value-of select="text()"/>]
</a>
</xsl:template>
</xsl:stylesheet>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Some files were not shown because too many files have changed in this diff Show More