in writing up a reply to http://drupal.org/node/137135, it occurs to me we've got 3 places where we want to compute the "diff" of the issue that's being generated by a comment/follow-up:
- project_issue_comment_validate() -- for the
// make sure this followup changes something or has a bodycode block - _project_comment_view_single() -- to display the HTML summary of changes when viewing an issue
- project_mail_notify() -- when generating the email for a given follow-up comment
i'd be very much in favor of refactoring this code so that a single function (e.g. "project_issue_comment_diff()"?) could be invoked by all 3, which returns an array of the diff, something like:
array(
'old' => array(
'component' => 'Code',
'sid' => 8,
'cck_field_name' => 'Foo',
),
'new' => array(
'component' => 'User interface',
'sid' => 13,
'cck_field_name' => 'Bar',
),
)
then, that part of the validate function is easy: if the array comes back empty, it's a validation error. the other 2 methods will become much more simple if they just have to iterate over the array and print out whatever visual stuff they want, to show what changed (the formatting is different for the html vs. email versions, but the functionality is basically identical). plus, this means we only have to change 1 place to modify what fields get treated in this way, e.g. for CCK fields (http://drupal.org/node/137135), potentially taxonomy terms (it'd be nice if when you associate taxonomy terms with issue nodes, that those show up on the issue followup form, too, and that you'd see when those changed via these same UI elements), etc, etc.
| Comment | File | Size | Author |
|---|---|---|---|
| #52 | pi_initial_metadata_diff.patch | 2.07 KB | dww |
| #50 | pi_metadata_diff.patch | 10.67 KB | hunmonk |
| #47 | project_issue-followup-diff-140473-47.patch | 9.88 KB | aclight |
| #47 | fake_testing_module.module_.txt | 660 bytes | aclight |
| #44 | project_issue-followup-diff-140473-44.patch | 10.15 KB | aclight |
Comments
Comment #1
dwwthis also relates to OG and http://drupal.org/project/og_project -- on a site where projects are site-wide, but issues are per-group, it'd be great to use followups to move an issue from 1 group to another. in that case, this project_issue_followup_diff() function would have to know about OG, too. not sure if we should make this a hook that other modules can implement, or not -- could be over-kill. but, it's another thing to consider when thinking about this problem.
Comment #2
aclight commentedHere's a first attempt at doing this. I'd like to get some feedback before I spend much more time on this, to make sure I'm headed in the right direction.
This patch basically does what dww suggested in the original issue, but it adds a bit more data into the array and also creates a new hook that other modules can implement so that they can add to the diff output. This was done with the possible comment_alter_taxonomy module (see #36 in http://drupal.org/node/187480) in mind but might also be used by OG_project.
The functions mentioned in the original issue have changed since ifac was added, but this patch creates the function project_issue_comment_diff() which is called by both _project_issue_comment_table() and project_mail_generate_followup_mail_body() to get the differences between two comments or a comment and original issue. The patch does not call project_issue_comment_diff() from project_issue_comment() to validate the comment and make sure that at least something has changed, because I'm not sure that we want to go through all the code in project_issue_comment() to build the entire history of the issue and then diff the most recent version of all metadata with that submitted via the comment form.
I haven't heavily tested this but based on my limited testing it seems to work well.
Comment #3
dwwSuper cool, thanks, aclight! I'll take a very close look at this in the near future.
One quick thought (thinking out loud): I wonder if my array structure from the original post is right. Instead of what I have above, would the following make it easier to interact with and manipulate these arrays in the code?
I'm really not sure, I haven't thought much about it after my initial brainstorm months ago. But, now that you've started hacking the code for it, I'm curious if you think this would be an improvement.
Also, yes, I'm totally fine with leaving the validation for empty comments out of this patch, and handling that over at http://drupal.org/node/190539.
Finally, have you looked at hook_diff? I wonder if it'd be possible to completely reuse that. Then, project_issue would have native support for diff.module, too, e.g. if someone just directly edits an issue node and changes metadata, instead of using a comment to alter something. Just a thought.
Thanks again, aclight!
Comment #4
dwwHaving briefly looked at hook_diff, it seems like this 'old', 'new' stuff is basically how that works, although it also expects 'name' and 'format' entries in each subarray...
It seems my 2nd array approach would simplify the return value from project_issue_comment_diff(), since instead of 4 cross-referenced associative arrays, we could just have a simple array of self-documenting associative arrays:
There's no need for a diff_keys array at all, those are just the keys of the top-level array. Seems much better this way, the more I think about it.
All that said, it might be impractical to directly implement hook_diff for this. However, given that diff.module already invokes hook_diff(), I'd be worried about naming our function project_issue_..._diff(), in case of possible conflicts with hook_diff() itself. If we're in our own world, perhaps "project_issue_comment_changes()"?
So, needs work since I think this new structure is better (sorry I didn't think of it before you started working on your patch), and since we should either a) directly implement hook_diff() and reuse it for our own needs (maybe), or b) rename our function to avoid potential collisions (more likely).
Thanks!
Comment #5
aclight commentedThis patch fixes a few things:
1. Changes name of new function to project_issue_comment_changes()
2. Changes name of new hook created by above to hook_followup_metadata_changes()
3. Hook functions called via module_invoke_all () can't use parameters passed by reference, so I added a global variable that's created in project_issue_comment_changes() that implementations of hook_followup_metadata_changes() can use.
The structure of the array returned by project_issue_comment_changes() looks like this:
[label_array] is initially populated with the returned array from _project_issue_comment_labels() or project_mail_summary_fields(). I'm not sure how we'd populate the name fields in your suggested array structure. I agree that [diff_keys] isn't strictly necessary, but having that makes it much easier to see what fields have changed--otherwise, this would require stepping through [old] or [new].
BTW, with an addition to comment_alter_taxonomy and a quick patch to project_issue that stores taxonomy metadata in {project_issue_comments}, I've got the tagging stuff all working on my test site (see screenshot attached).
Given my reasoning behind structuring the changes array as I have, I'll set this back to cnr. If you can suggest an elegant way to get the field names in place while still using your array structure then I'll reconsider :)
Comment #6
beginner commentedI tested the patch. It applies cleanly (though with -p1 from within the module directory).
Follow up comments work as before.
I don't know how to set up a mail server on my own computer so that I can test the mail notification. Is that difficult to do (on a linux box)?
Is $project_issue_comment_project_issue_comment_changes_array a typo?
Comment #7
beginner commentedAnother typo just below in the same block, point 4. : changeserences
Comment #8
beginner commentedchangeserences actually appears 3 times in the patch.
???
Comment #9
aclight commented@beginner: Thanks for taking a look at this patch. I'll have to check into the potential typos you brought up. As for setting up a linux box to send emails, I use a linux box for development (Ubuntu 7.04) and I think it just came configured to send mail. If I did have to set it up for mail, that would have been over a year ago and I don't remember what I had to do to get it set up though.
I'll try to get an updated patch posted sometime today.
Comment #10
beginner commentedMaybe the mail notification does need a bit more testing.
I am running this patch on semi-production site that I just set up.
On a follow up, I received a mail with the following setting changes (while only 1 setting was actually changed):
It's a shame because this issue is holding up that one: http://drupal.org/node/187480
Comment #11
aclight commented@beginner: Are you sure you have your test site set up correctly? The emails I've gotten on my test site are correct and do not indicate that every field has changed, as you suggest. Can you add a deubgging print_r statement in project_issue_comment_changes() to see if the changes function itself is malfunctioning? I will hopefully have some time to work on this this weekend.
Thanks again for testing this.
Comment #12
beginner commentedI did some more testing. Removing the patch fixes the problem.
The problem occurs only with the 1st comment of every issue. It's logical: the array of issue setting has not been initialized yet.
There is no problem with any subsequent comment.
Comment #13
aclight commented@beginner: Thanks for pointing out the typos. Those all came from a search and replace where a bit too much got replaced.
As for mail notification, I can't replicate your problem with a clean drupal install and a clean project_issue install with my patch from #10. I tried creating a new issue, the first reply (comment) to an issue, and additional replies and at no point did I get the werid diff you got. My only guess is that it has something to do with the fact that in your case it doesn't look like you have assigned a project or author to the issue.
Just as an example, here's an example of a new original issue:
and here's the first reply to the issue.
I did notice that in the new diff function I was getting PHP errors for the first issue, and so I added a bit of additional checking that gets rid of the errors. Maybe that also fixes the problem you're reporting?
Comment #14
beginner commentedI am going to test the patch soon.
What PHP version do you use?
Comment #15
aclight commented@beginner: 5.2.3 on one system and 5.2.1 on another.
Comment #16
beginner commentedThat might be the difference. I have php 4.4.6.
I am not an expert in those things, so I don't know if, why and how it is the cause.
My install is fresh. Drupal updated to 5.5, project* installed from CVS (D5-head) barely a week ago.
I experience the same problem with the (new) patch, while without it, everything seems normal.
Notice that none of the values are set:
Comment #17
beginner commentedIf in line 391 of mail.inc I print_r the array, I notice that $comment_change['new'] is such that for each key, the value is empty.
Comment #18
aclight commentedI just tried my patch using PHP 4.4.7 and got the same results I get with 5.2.:
I'm not sure what's going on here.
Comment #19
aclight commented@beginner
Add
dpm($comment_changes);to line 386 of the patched mail.inc. That'll now look like this:Create a new issue, then create a followup to the issue. Paste the printout of the array here. This is what mine looks like, using PHP 5.2:
Comment #20
beginner commentedFatal error: Call to undefined function: dpm() in mail.inc on line 386
What is dmp()?
I am going to try with print_r();
I apologize for all the trouble I am causing. I wanted to help this patch move along, not hinder it. My problem is a fairly minor cosmetic issue that you cannot even reproduce. If Dereck wants to commit the patch, go ahead :)
Anyway, I am going to test some more...
Comment #21
aclight commented@beginner: Sorry, dpm() is in devel.module. It's basically the same as print_r() put displays the information in a more friendly way.
No need to appologize for causing trouble--I believe what you're saying, I'm just not understanding why you're seeing different behavior than I do. I'm sure we'll get to the bottom of this soon :)
Comment #22
beginner commentedThanks :)
As mentioned earlier, I am not set up locally to test mails. The bug I am reporting has been tested and reproduced many times on the remote server. I updated project and project_issue one hour ago to the latest in DRUPAL-5 branch. So, I am up to date both locally and remotelly and I still could reproduce the bug. Drupal core has been updated recently. Still, there is the remote possibility that there are differences between the two setups. I can easily test print_r() kind of stuff locally. If need be, I'll test it on the remote server, too.
This being said, here is what I get locally on a one hour old Drupal install:
When I create a new issue:
Here is the 4th comment follow up in an issue:
Here is the 1st comment follow up in an issue:
The title field seems to be missing from the array, even though i changed it. But it's missing from yours, too.
I don't see anything significantly different.
I am going to test on the remote server.
Comment #23
beginner commentedEhhhh... ???
On my remote setup, project_mail_generate_followup_mail_body() doesn't even seem to be called. When posting a new issue, i get the print_r() output, but when posting a follow up (1st, 2nd...), nothing is printed at all... But I do receive the mails...
Comment #24
aclight commented@beginner: try installing devel.module and use dpm(). Your print_r() might be printed but then you're redirected somewhere else and you never actually see it. With dpm() I don't think that will be a problem.
Comment #25
beginner commentedThings are weird.
To be thourough, I added the following, so that I am sure something is outputed:
Locally, something WAS outputed on a new issue, a 1st comment and a 2nd.
On the remote server, something was output on a new issue, on a 1st comment but NOT on a 2nd comment ???
I receive all the mails nonetheless and always it is the 1st comment that has a weird formatting.
I tried a second time to make sure.
I have the expected output when posting a new issue, a 1st comment.
But I have no output on screen when posting a 2nd or 3rd comment but I still receive the mails.
I can try with devel.module as you suggest...
Comment #26
aclight commented@beginner: Can you try commenting out the global line in project_issue_comment_changes(). I wonder if somehow that's causing problems.
Comment #27
beginner commentedOk. I'll try that next.
Meanwhile, here is the output with dpm():
1st comment:
2nd comment:
Mails are always the same...
Comment #28
beginner commentedI seem to be getting more consistent results with dpm() (i.e. I have some output each time).
Trying with:
No significant change that I could see.
Comment #29
beginner commentedI doubt it's relevant but... my install is 1 week old. There is only few core modules, project* and locale.module. I have translated some terms (as you can see) using the project_issue setting pages. That's about it. I actually tried once with locale.module disabled, but i got the same... so I really don't see.
@dereck: what do you think? I may have hit some very corner case and as long as I am the only one experiencing this, it's difficult to debug. I could do better testing on my own without bothering anybody if I could have a local setup with mail. But ftp'ing the files after each minor debug change is time consuming...
I suggest that this minor issue doesn't hold up this patch. If others experience it later, then we'd have a better basis to figure out what's wrong.
I'm moving on to other things until dww has had a chance to comment.
Thank you aclight for bearing up with me and guiding me :)
Comment #30
aclight commented@beginner: Have you tried this patch out on a clean copy of the Drupal.org testing installation profile? That's what I've been using to test this patch, so it might be nice if you could try that as well since then we'd at least be using the same code. I just wrote a handbook page yesterday about how to use this profile. You'll need access to a CVS client, however, as there is not yet a release for the profile.
Also, what OS is each of your systems using? That might be relevant, though I am not sure how.
As for your testing environments, it sounds like you could optimize your work flow a little better. I don't know what text editor you are using, but one of the features I always look for in a text editor is the ability to read and write remote files via FTP or SFTP. I just open the file on the server remotely on whatever computer I happen to be using at the time. Any changes to the file are automatically uploaded back to the server when I save the file. That makes debugging a lot easier.
As for testing mail, this again depends on what OS you're using. I have a new development machine running MacOS. It doesn't appear to be able (out of the box) to send project_issue emails to a remote address, but I have set my test account's address to be username@loaclhost. I can then use the "mail" program from within Terminal.app to read messages sent by project issue. There is probably an even better way to do this, but my background is more PC/Linux oriented so I'm still figuring out how to do things on the Mac.
I think it's a bit premature to call this an edge case. As far as I know, you and I are the only person to test this patch yet. So potentially 1/2 of the people testing the patch are getting strange results. That's not a good thing :)
BTW, if you use IRC, feel free to ping me on #drupal.
Comment #31
aclight commented@beginner: I'm sure you'll be glad to know that I am now able to reproduce this problem. I'm really not sure WHY I can reproduce it now and not before, however.
I'm setting the status to cnw until I can take a look and figure out where the problem is coming from.
Comment #32
aclight commented@beginner: Do you by any chance have the core Book module enabled on your test site(s) where you're getting metadata diff problems in the emails?
Comment #33
aclight commentedOk, I think I've fixed this problem. I'm not 100% sure why this is happening, but from what I can tell there is a conflict with the book.module (that may be related to this issue: http://drupal.org/node/98278) such that when book_nodeapi() fires, a property called '0' is added to the $node object. To see this for yourself, do the following:
1. Download and install a clean site using the Drupal.org testing profile.
2. Login to the site.
3. Subscribe yourself to all project issues.
4. Open up the project_issue/mail.inc file, and on line 385 add a
dpm($previous);statement, so that you get:5. Click on a project issue that has zero comments, click add a comment, put something in the comment text box, and click submit.
When dpm() prints out the node, towards the bottom you'll see this:
If you disable book.module, this won't happen.
So, now that I sort of figured why this was happening, fixing it is pretty easy. I just added the
strict = TRUEparameter to in_array() in project_mail_generate_followup_mail_body():This seems to fix the problem for me.
@beginner: Can you please test this updated patch and see if it also fixes the problem for you?
Comment #34
beginner commentedOk! That's great :)
I'll test the new patch later today.
For now I can confirm that I had book.module enabled on the remote site.
Comment #35
beginner commentedYou can indeed see the
[0] =>bit in my output above.Comment #36
beginner commentedI tried patch #13 with the book.module disabled the bug didn't appear.
I tried the patch #33 with the book.module enabled and everything works fine.
The patch itself looks good. Typos have been fixed.
-> RTBC
What follows is blah blah... :)
@aclight
good job. It's good you were not as ready to give up as I was... :)
To answer your questions:
I am using Mandriva 2006, though I am looking forward to figuring out what prevents me from upgrading to Debian Etch. Until I do, I am stuck with php 4, mysql 4.0, etc. :(
I use vim and lftp. I have seen somewhere a patch for lftp so that it has vim support integrated. Thus I could do what you are saying (file editing by ftp). Maybe I'll try to patch and compile lftp myself once I've upgraded to Debian.
But it can only help that much: I am paranoid as far as ftp password is concerned. The very first web site (all html) that I have ever uploaded on a remote server - 7~8 years ago - was hacked only hours later because a guy sniffed my ftp login. Now, not having ssh, sftp nor anything secure at my host, I change my ftp password after each ftp opperation. So, debugging on a remote server would remain a combersome process.
So, for this and other reasons I do need to setup a minimal web server for testing mails. I have already installed the postfix and postfix-mysql rpm's but I am still not sure how to configure them. I'll write to the dev list about this. No need to reply here.
dpm() is cool. I should use devel.module more. I'll check the profile you're suggesting, too. Thanks.
Let's get this patch in, and back to the other issue : http://drupal.org/node/187480
Blessings,
:)
Comment #37
dwwThanks to both of you for sorting this out, great work!
However, looking closely at this patch, I'm more convinced than ever of what I said in #4 above. I don't understand this part of comment #5:
My comment exactly explained how to get the field names in place, they're just another element of the nested associative array.
Here's another example:
Could be replaced with:
The latter is clearly more simple, and project* needs a healthy dose of simple if it's going to survive.
Thanks,
-Derek
Comment #38
aclight commentedSimple? Since when is project* simple? :)
Attached patch changes the changes array to look like this:
* array(
* 'component' => array(
* 'name' => t('Component'),
* 'change_type' => 'modify';
* 'old' => 'Code',
* 'new' => 'User interface',
* ),
* 'sid' => array(
* 'name' => t('Status'),
* 'change_type' => 'modify';
* 'old' => 8,
* 'new' => 13,
* ),
* )
as suggested in comment #4. I did add another property, 'change_type', because it makes the logic about whether there is a change simpler to code and I think more readable. Plus, in the future, there might be a place to display 'add', 'delete', and 'modify' differently.
I'm not sure this really simplifies the code that much, but I did shorten some of the variable names so that helps out some.
@dww: is this closer to what you're looking for?
Comment #39
dwwHehe. I said it "needs a healthy dose of simple", not that it was already simple. See, for example: http://drupal.org/node/199138 as a step in the right direction. ;)
Anyway, let's see if this patch is closer...
Comment #40
dwwA) I don't see a good reason for change_type. If 'old' is defined and 'new' is empty, it's "remove". If both are defined, it's "modify", and if 'old' is empty and 'new' is defined, it's "add". If there's no change, there shouldn't be a nested array for that attribute at all, IMHO. Also, the patch is inconsistent with, for example, 'none' vs. 'NONE'. Why bother?
B) project_mail_summary() is a pretty crappy name for this function at this point, since we're using it for many things that aren't mail. ;) What about my suggestion above for
project_issue_change_summary($change, 'new')?C) What's up with this: "Use the global variable $project_issue_comment_changes_array". Ugh, that seems gross. Can't we do better? What's wrong this?
That way, the hooks can use a reference for the first argument...
D) Why is $field_labels an argument to project_issue_comment_changes()? Oh, and good grief, why do we have both _project_issue_comment_labels() and project_mail_summary_fields()? Evil! ;) In fact, let's just clean up that minor mess in a quick separate issue, and re-roll this once that lands, ok?
Comment #41
dwwPoint D moved to http://drupal.org/node/200097 -- Let's resolve that first and then come back here for a re-roll.
Comment #42
dww#200097 is now fixed, so this is active again.
Comment #43
aclight commentedThis is true for the tables displayed on the issues when viewed via the web, but not true in issue emails, since some fields, such as Assigned to, Status, Reported by, and Updated by need to be printed regardless of whether the value of that field has changed.
But, as you requested, I took out the 'change_type' element of each nested array in project_issue_comment_changes(). Instead of checking for the value of 'change_type' I'm now just testing to see if 'new' and 'old' are defined, and if not that means there isn't a change and therefore those fields aren't printed.
B.) Agreed. I changed the name of project_mail_summary() to project_issue_change_summary(), and moved the function from mail.inc to issue.inc.
C.) Good idea. I've changed the hook call to use what you suggested, and that works fine.
D.) This has already been handled in #200097: Remove duplicate code for issue attribute labels.
I tested this pretty thoroughly and everything seems to be working well.
For anyone who wants to test this, I recommend using the Drupal.org testing installation profile at http://drupal.org/project/drupalorg_testing (CVS only).
For testing the new hook, I've also attached a simple .module and .info file that you can download and enable. The module is called "Fake testing module" and is in the Project group. All the module does is implement the hook added in this patch. NOTE: The test module will spit out the same thing every time. So, in other words, for each comment, you'll see a Vocabulary 10:, Vocabulary 11:, and Vocabulary 12: field, even if you don't have such vocabularies on your site or if they have nothing to do with project issues.
Comment #44
aclight commentedThis is the same patch as #43, but I realized that since project_issue_comment_changes() isn't using a global variable anymore, the name of the variable doesn't have to be so unique since there are no namespace issues anymore. So, I shortened the name of the array to just $changes. Other than that search and replace, there are no changes in this patch.
Comment #45
hunmonk commentedi'd like dww to weigh in on my thoughts below before we go any further:
Comment #46
dwwRe 1: i'm not insistent that project_issue implements its own code for this in a hook, but i don't mind if it does. lots of code in Drupal handles the logic for something, then exposes an alter hook for others to play with. i don't see any burning need to make the "main" function a simple hook invocation spot, and then put the bulk of the logic into pi's hook implementation. but, if it makes hunmonk happier, i don't see anything wrong with this approach, either. ;) and, it means we'd be eating our own API dogfood, which would help us notice problems, such as...
Re 2: yup, i agree. we need to pass, at the bare minimum, an issue nid to this hook, and probably the comment id (if defined), too. else, how would anyone implementing the hook know how to find their extra data in a table somewhere to add to the array? especially since the most important field, issue nid, never changes in an issue, and therefore, can never be assumed to be imbedded in the array of changes at some well known location. ;)
thanks, folks.
Comment #47
aclight commentedGood point about adding the project_issue node into the call to project_issue_comment_changes(). Since the project issue node is already loaded in both places where picc() is called, I figured it made sense to just pass in the entire $node object, instead of passing in just $node->nid. I can change that to just $node->nid if you think that's better.
As for the comment id, that will always be available in $old_data and $new_data if we're looking at a comment. In the case where we're doing the diff between the original issue and the first comment. So, $old_data['cid'] would not exist as an element of the $old_data array, but $new_data['cid']=XXX would exist. At the point where picc() is called, the comment object is not necessarily loaded yet.
I talked with hunmonk in IRC about whether or not project_issue should implement this new hook itself. Because the metadata fields will be printed out on the table or email in the order they are in the $changes array, we really want project_issue to compute its changes first since we want those fields to be at the top of any display. Thus, I think it makes sense to NOT have project_issue implement this hook itself. This way we ensure that these fields are at the top of the array.
[Edit: There's also a new copy of fake_testing_module.module that implements the hook in the slightly new way. The fake_testing_module.info file in http://drupal.org/node/140473#comment-698876 can still be used.]
Comment #48
webchicksubscribing
Comment #49
hunmonk commentedcode style looks good. the implementation is good.
however, i get some pretty funky output when i try to preview a comment that has metadata changes:
Comment #50
hunmonk commentedcouple of small things:
fixed both of those, and committed the attached patch to 5.x-2.x
Comment #51
dwwPretty sure this is what broke http://drupal.org/node/214032.
Comment #52
dwwLooking closely at the patch that was committed, there are 3 bugs here:
A) project_issue_comment_changes() in comment.inc does this:
yet project_mail_generate_followup_mail_body() in mail.inc does this:
if (!empty($change['label']) && isset($change['old']) && isset($change['new']) ...)'' counts as isset(), so we need to either not define 'old' or 'net' in project_issue_comment_changes() or use !empty() in project_mail_generate_followup_mail_body(). !empty() would be wrong for attributes that can be a 0, so we should probably stick with isset() and not bloat the $changes array with the "empty" changes when adding or removing an attribute. Speaking of which, how do you ever remove something? Isn't "adding" only true on the initial issue, anyway?
B) The $changes array (on initial issue submit) contains entries (mostly without labels), for every property in the $node object.
C) The $changes array for initial issues contains 'rid' even if the project has no releases at all.
Furthermore, the existing code seems needlessly complicated.
Attached patch fixes all 3 and simplifies a lot. I believe this is what we really want, and it seems to work fine in my testing, but I'd like comments from aclight and/or hunmonk before I commit or deploy on d.o. Thanks.
Comment #53
hunmonk commentedcode style looks good. gave it a pretty hard workover, seems to work for both web and mail output, with or without releases.
Comment #54
aclight commentedAgreed. Code looks good and behaves as expected.
Comment #55
dwwCommitted to HEAD and deployed on d.o. Thanks for the reviews, folks.
Comment #56
Anonymous (not verified) commentedAutomatically closed -- issue fixed for two weeks with no activity.