Indefero

Indefero Commit Details


Date:2011-05-30 07:02:10 (13 years 6 months ago)
Author:Thomas Keller
Branch:develop, feature.content-md5, feature.diff-whitespace, feature.issue-links, feature.issue-of-others, feature.issue-summary, feature.search-filter, feature.webrepos, feature.wiki-default-page, release-1.2, release-1.3
Commit:0aa5999bb37698513d66453d98101232942a6af3
Parents: 16dda0743c805fa01bc2d0a313c89436f5b3080c
Message:Finalize ticket relations (closes issue 638)

- IssueUpdate.php: use dynamically set field validators for dynamically
created fields; let relation_type0 and relation_issue0 exist at any time;
check the validity of a user selection and combine the various input fields
if possible; do the database updates for links; change the "change" format
for labels to a more precise structure and no longer trust on a leading
dash for removed labels
- IssueCreate.php: change the validator calls and field names
- Issue.php (getGroupedRelatedIssues): make it possible to return only a
flat list of integers for easier processing
- 17AddIssueRelations.php: migrate the previous serialized "changes" format
for issue comments to the new, more structured format (up and down)
- js-autocomplete.html: add support for multiple input fields
- view.html: output relation changes and wrap the related issues stanzas into
paragraphs
- NEWS.mdtext: note the addition and the need for a specific version of Pluf
Changes:

File differences

NEWS.mdtext
11
22
3
4
5
36
47
5
8
9
10
11
612
713
814
# InDefero 1.2 - xxx xxx xx xx:xx 2011 UTC
ATTENTION: You need Pluf [324ae60b](http://projects.ceondo.com/p/pluf/source/commit/324ae60b)
or newer to properly run this version of Indefero!
## New Features
## Bugfixes
- Indefero's issue tracker can now bi-directionally link issues with variable, configurable
terms, such as "is related to", "is blocked by" or "is duplicated by" (issue 638)
## Bugfixes
- monotone zip archive entries now all carry the revision date as mtime (issue 645)
- Timeline only displays filter options for items a user has actually access to (issue 655)
src/IDF/Form/IssueCreate.php
113113
114114
115115
116
116
117117
118118
119119
120120
121121
122122
123
123
124124
125125
126126
......
253253
254254
255255
256
256
257
258
257259
258
260
259261
260262
261263
......
272274
273275
274276
275
277
276278
277
279
280
281
282
283
284
285
286
278287
279288
280289
......
296305
297306
298307
308
309
310
311
312
299313
300314
301315
......
360374
361375
362376
363
377
364378
365
379
366380
367381
368382
),
));
$this->fields['relation_type'] = new Pluf_Form_Field_Varchar(
$this->fields['relation_type0'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => current($this->relation_types),
'widget_attrs' => array('size' => 15),
));
$this->fields['relation_issue'] = new Pluf_Form_Field_Varchar(
$this->fields['relation_issue0'] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => '',
return $this->cleaned_data['status'];
}
function clean_relation_type()
// this method is not called from Pluf_Form directly, but shared for
// among all similar fields
function clean_relation_type($value)
{
$relation_type = trim($this->cleaned_data['relation_type']);
$relation_type = trim($value);
if (empty($relation_type))
return '';
return $relation_type;
}
function clean_relation_issue()
function clean_relation_type0()
{
$issues = trim($this->cleaned_data['relation_issue']);
return $this->clean_relation_type($this->cleaned_data['relation_type0']);
}
// this method is not called from Pluf_Form directly, but shared for
// among all similar fields
function clean_relation_issue($value)
{
$issues = trim($value);
if (empty($issues))
return '';
return implode(', ', $issue_ids);
}
function clean_relation_issue0()
{
return $this->clean_relation_issue($this->cleaned_data['relation_issue0']);
}
/**
* Clean the attachments post failure.
*/
$issue->setAssoc($tag);
}
// add relations
$verb = $this->cleaned_data['relation_type'];
$verb = $this->cleaned_data['relation_type0'];
$other_verb = $this->relation_types[$verb];
$related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'], -1, PREG_SPLIT_NO_EMPTY);
$related_issues = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue0'], -1, PREG_SPLIT_NO_EMPTY);
foreach ($related_issues as $related_issue_id) {
$related_issue = new IDF_Issue($related_issue_id);
$rel = new IDF_IssueRelation();
src/IDF/Form/IssueUpdate.php
104104
105105
106106
107
107
108
109
110
108111
109112
110113
111114
112115
113116
114
117
115118
116119
117120
118121
119122
120123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
121152
122153
123154
......
171202
172203
173204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
174247
175248
176249
......
230303
231304
232305
306
307
308
309
310
233311
234312
235313
......
271349
272350
273351
352
274353
275
354
276355
277
356
278357
279358
280359
281360
282361
283362
363
284364
285
365
286366
287
367
288368
289369
290370
......
302382
303383
304384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
305426
306427
307428
),
));
$this->fields['relation_type'] = new Pluf_Form_Field_Varchar(
$idx = 0;
// note: clean_relation_type0 and clean_relation_issue0 already
// exist in the base class
$this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => current($this->relation_types),
'widget_attrs' => array('size' => 15),
));
$this->fields['relation_issue'] = new Pluf_Form_Field_Varchar(
$this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => '',
'widget_attrs' => array('size' => 10),
));
++$idx;
$relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true);
foreach ($relatedIssues as $verb => $ids) {
$this->fields['relation_type'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => __('This issue'),
'initial' => $verb,
'widget_attrs' => array('size' => 15),
));
$m = 'clean_relation_type'.$idx;
$this->$m = create_function('$form', '
return $form->clean_relation_type($form->cleaned_data["relation_type'.$idx.'"]);
');
$this->fields['relation_issue'.$idx] = new Pluf_Form_Field_Varchar(
array('required' => false,
'label' => null,
'initial' => implode(', ', $ids),
'widget_attrs' => array('size' => 10),
));
$m = 'clean_relation_issue'.$idx;
$this->$m = create_function('$form', '
return $form->clean_relation_issue($form->cleaned_data["relation_issue'.$idx.'"]);
');
++$idx;
}
$tags = $this->issue->get_tags_list();
for ($i=1;$i<7;$i++) {
$initial = '';
public function clean()
{
$this->cleaned_data = parent::clean();
// normalize the user's input by removing dublettes and by combining
// ids from identical verbs in different input fields into one array
$normRelatedIssues = array();
for ($idx = 0; isset($this->cleaned_data['relation_type'.$idx]); ++$idx) {
$verb = $this->cleaned_data['relation_type'.$idx];
$ids = preg_split('/\s*,\s*/', $this->cleaned_data['relation_issue'.$idx],
-1, PREG_SPLIT_NO_EMPTY);
if (count($ids) == 0)
continue;
if (!array_key_exists($verb, $normRelatedIssues))
$normRelatedIssues[$verb] = array();
foreach ($ids as $id) {
if (!in_array($id, $normRelatedIssues[$verb]))
$normRelatedIssues[$verb][] = $id;
}
}
// now look at any added / removed ids
$added = $removed = array();
$relatedIssues = $this->issue->getGroupedRelatedIssues(array(), true);
$added = array_diff_key($normRelatedIssues, $relatedIssues);
$removed = array_diff_key($relatedIssues, $normRelatedIssues);
$keysToLookAt = array_keys(
array_intersect_key($relatedIssues, $normRelatedIssues)
);
foreach ($keysToLookAt as $key) {
$a = array_diff($normRelatedIssues[$key], $relatedIssues[$key]);
if (count($a) > 0)
$added[$key] = $a;
$r = array_diff($relatedIssues[$key], $normRelatedIssues[$key]);
if (count($r) > 0)
$removed[$key] = $r;
}
// cache the added / removed data, so we do not have to
// calculate that again
$this->cleaned_data['_added_issue_relations'] = $added;
$this->cleaned_data['_removed_issue_relations'] = $removed;
// As soon as we know that at least one change was done, we
// return the cleaned data and do not go further.
if (strlen(trim($this->cleaned_data['content']))) {
return $this->cleaned_data;
}
}
if (count($this->cleaned_data['_added_issue_relations']) != 0 ||
count($this->cleaned_data['_removed_issue_relations']) != 0) {
return $this->cleaned_data;
}
}
// no changes!
throw new Pluf_Form_Invalid(__('No changes were entered.'));
foreach ($tags as $tag) {
if (!Pluf_Model_InArray($tag, $oldtags)) {
if (!isset($changes['lb'])) $changes['lb'] = array();
if (!isset($changes['lb']['add'])) $changes['lb']['add'] = array();
if ($tag->class != 'Other') {
$changes['lb'][] = (string) $tag; //new tag
$changes['lb']['add'][] = (string) $tag; //new tag
} else {
$changes['lb'][] = (string) $tag->name;
$changes['lb']['add'][] = (string) $tag->name;
}
}
}
foreach ($oldtags as $tag) {
if (!Pluf_Model_InArray($tag, $tags)) {
if (!isset($changes['lb'])) $changes['lb'] = array();
if (!isset($changes['lb']['rem'])) $changes['lb']['rem'] = array();
if ($tag->class != 'Other') {
$changes['lb'][] = '-'.(string) $tag; //new tag
$changes['lb']['rem'][] = (string) $tag; //new tag
} else {
$changes['lb'][] = '-'.(string) $tag->name;
$changes['lb']['rem'][] = (string) $tag->name;
}
}
}
or ((!is_null($owner) and !is_null($this->issue->get_owner())) and $owner->id != $this->issue->get_owner()->id)) {
$changes['ow'] = (is_null($owner)) ? '---' : $owner->login;
}
// Issue relations - additions
foreach ($this->cleaned_data['_added_issue_relations'] as $verb => $ids) {
$other_verb = $this->relation_types[$verb];
foreach ($ids as $id) {
$related_issue = new IDF_Issue($id);
$rel = new IDF_IssueRelation();
$rel->issue = $this->issue;
$rel->verb = $verb;
$rel->other_issue = $related_issue;
$rel->submitter = $this->user;
$rel->create();
$other_rel = new IDF_IssueRelation();
$other_rel->issue = $related_issue;
$other_rel->verb = $other_verb;
$other_rel->other_issue = $this->issue;
$other_rel->submitter = $this->user;
$other_rel->create();
}
if (!isset($changes['rel'])) $changes['rel'] = array();
if (!isset($changes['rel']['add'])) $changes['rel']['add'] = array();
$changes['rel']['add'][] = $verb.' '.implode(', ', $ids);
}
// Issue relations - removals
foreach ($this->cleaned_data['_removed_issue_relations'] as $verb => $ids) {
foreach ($ids as $id) {
$db = &Pluf::db();
$table = Pluf::factory('IDF_IssueRelation')->getSqlTable();
$sql = new Pluf_SQL('verb=%s AND (
(issue=%s AND other_issue=%s) OR
(other_issue=%s AND issue=%s))',
array($verb,
$this->issue->id, $id,
$this->issue->id, $id));
$db->execute('DELETE FROM '.$table.' WHERE '.$sql->gen());
}
if (!isset($changes['rel'])) $changes['rel'] = array();
if (!isset($changes['rel']['rem'])) $changes['rel']['rem'] = array();
$changes['rel']['rem'][] = $verb.' '.implode(', ', $ids);
}
// Update the issue
$this->issue->batchAssoc('IDF_Tag', $tagids);
$this->issue->summary = trim($this->cleaned_data['summary']);
src/IDF/Issue.php
169169
170170
171171
172
172
173173
174174
175175
......
181181
182182
183183
184
184
185185
186186
187187
}
}
function getGroupedRelatedIssues($opts = array())
function getGroupedRelatedIssues($opts = array(), $idsOnly = false)
{
$rels = $this->get_related_issues_list(array_merge($opts, array(
'view' => 'with_other_issue',
if (!array_key_exists($verb, $res)) {
$res[$verb] = array();
}
$res[$verb][] = $rel;
$res[$verb][] = $idsOnly ? $rel->other_issue : $rel;
}
return $res;
src/IDF/IssueComment.php
155155
156156
157157
158
159
158160
159161
160
161
162
163
164
165
166
167
168
169
162170
163171
164172
$out .= __('Owner:'); break;
case 'lb':
$out .= __('Labels:'); break;
case 'rel':
$out .= __('Relations:'); break;
}
$out .= '</strong>&nbsp;';
if ($w == 'lb') {
$out .= Pluf_esc(implode(', ', $v));
if ($w == 'lb' || $w == 'rel') {
foreach ($v as $t => $ls) {
foreach ($ls as $l) {
if ($t == 'rem') $out .= '<s>';
$out .= Pluf_esc($l);
if ($t == 'rem') $out .= '</s> ';
}
}
} else {
$out .= Pluf_esc($v);
}
src/IDF/Migrations/17AddIssueRelations.php
3232
3333
3434
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
3556
3657
3758
......
4061
4162
4263
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
4389
4490
$schema = new Pluf_DB_Schema($db);
$schema->model = new IDF_IssueRelation();
$schema->createTables();
// change the serialization format for added / removed labels in IDF_IssueComment
$comments = Pluf::factory('IDF_IssueComment')->getList();
foreach ($comments as $comment) {
if (!isset($comment->changes['lb'])) continue;
$changes = $comment->changes;
$adds = $removals = array();
foreach ($comment->changes['lb'] as $lb) {
if (substr($lb, 0, 1) == '-')
$removals[] = substr($lb, 1);
else
$adds[] = $lb;
}
$changes['lb'] = array();
if (count($adds) > 0)
$changes['lb']['add'] = $adds;
if (count($removals) > 0)
$changes['lb']['rem'] = $removals;
$comment->changes = $changes;
$comment->update();
}
}
function IDF_Migrations_17AddIssueRelations_down($params=null)
$schema = new Pluf_DB_Schema($db);
$schema->model = new IDF_IssueRelation();
$schema->dropTables();
// change the serialization format for added / removed labels in IDF_IssueComment
$comments = Pluf::factory('IDF_IssueComment')->getList();
foreach ($comments as $comment) {
$changes = $comment->changes;
if (empty($changes))
continue;
if (isset($changes['lb'])) {
$labels = array();
foreach ($changes['lb'] as $type => $lbs) {
if (!is_array($lbs)) {
$labels[] = $lbs;
continue;
}
foreach ($lbs as $lb) {
$labels[] = ($type == 'rem' ? '-' : '') . $lb;
}
}
$changes['lb'] = $labels;
}
// while we're at it, remove any 'rel' changes
unset($changes['rel']);
$comment->changes = $changes;
$comment->update();
}
}
src/IDF/Views/Issue.php
404404
405405
406406
407
407
408408
409409
410410
$issue = Pluf_Shortcuts_GetObjectOr404('IDF_Issue', $match[2]);
$prj->inOr404($issue);
$comments = $issue->get_comments_list(array('order' => 'id ASC'));
$related_issues = $issue->getGroupedRelatedIssues(array('order' => 'creation_dtime DESC'));
$related_issues = $issue->getGroupedRelatedIssues(array('order' => 'other_issue ASC'));
$url = Pluf_HTTP_URL_urlForView('IDF_Views_Issue::view',
array($prj->shortname, $issue->id));
src/IDF/templates/idf/issues/create.html
6363
6464
6565
66
66
6767
68
69
70
71
68
69
70
71
7272
7373
7474
</td>
</tr>
<tr>
<th>{$form.f.relation_type.labelTag}:</th>
<th>{$form.f.relation_type0.labelTag}:</th>
<td>
{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if}
{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if}
{$form.f.relation_type|unsafe}
{$form.f.relation_issue|unsafe}
{if $form.f.relation_type0.errors}{$form.f.relation_type0.fieldErrors}{/if}
{if $form.f.relation_issue0.errors}{$form.f.relation_issue0.fieldErrors}{/if}
{$form.f.relation_type0|unsafe}
{$form.f.relation_issue0|unsafe}
</td>
</tr>
<tr>
src/IDF/templates/idf/issues/js-autocomplete.html
5151
5252
5353
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
8287
8388
8489
return row.to;
}
});
$("#id_relation_type").autocomplete(auto_relation_types, {
minChars: 0,
width: 310,
matchContains: true,
max: 50,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row.to.replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row.name + "</span>";
},
formatResult: function(row) {
return row.to;
}
});
$("#id_relation_issue").autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", {
minChars: 0,
width: 310,
matchContains: true,
max: 10,
multiple: true,
delay: 500,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row[1].replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row[0] + "</span>";
},
formatResult: function(row) {
return row[1];
}
});
for (var idx = 0; ; ++idx) {
if ($("#id_relation_type" + idx).length == 0)
break;
$("#id_relation_type" + idx).autocomplete(auto_relation_types, {
minChars: 0,
width: 310,
matchContains: true,
max: 50,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row.to.replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row.name + "</span>";
},
formatResult: function(row) {
return row.to;
}
});
$("#id_relation_issue" + idx).autocomplete("{/literal}{url 'IDF_Views_Issue::autoCompleteIssueList', array($project.shortname, $issue.id)}{literal}", {
minChars: 0,
width: 310,
matchContains: true,
max: 10,
multiple: true,
delay: 500,
highlightItem: false,
formatItem: function(row, i, max, term) {
return row[1].replace(new RegExp("(" + term + ")", "gi"), "<strong>$1</strong>") + " <span style='font-size: 80%;'>" + row[0] + "</span>";
},
formatResult: function(row) {
return row[1];
}
});
}
});
{/literal} //-->
</script>
src/IDF/templates/idf/issues/view.html
4040
4141
4242
43
43
44
45
46
47
48
49
50
51
52
4453
4554
4655
......
120129
121130
122131
123
132
124133
125
126
127
128
134
135
136
137
138
139
140
141
142
143
144
145
129146
130147
131148
......
172189
173190
174191
192
175193
176194
177195
178196
179
197
180198
181199
182200
183201
202
184203
185204
186205
{if $i> 0 and $c.changedIssue()}
<div class="issue-changes">
{foreach $c.changes as $w => $v}
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}</strong> {if $w == 'lb'}{assign $l = implode(', ', $v)}{$l}{else}{$v}{/if}<br />
<strong>{if $w == 'su'}{trans 'Summary:'}{/if}{if $w == 'st'}{trans 'Status:'}{/if}{if $w == 'ow'}{trans 'Owner:'}{/if}{if $w == 'lb'}{trans 'Labels:'}{/if}{if $w == 'rel'}{trans 'Relations:'}{/if}</strong>
{if $w == 'lb' or $w == 'rel'}
{foreach $v as $t => $ls}
{foreach $ls as $l}
{if $t == 'rem'}<s>{/if}{$l}{if $t == 'rem'}</s> {/if}
{/foreach}
{/foreach}
{else}
{$v}
{/if}<br />
{/foreach}
</div>
{/if}
</td>
</tr>
<tr>
<th>{$form.f.relation_type.labelTag}:</th>
<th>{$form.f.relation_type0.labelTag}:</th>
<td>
{if $form.f.relation_type.errors}{$form.f.relation_type.fieldErrors}{/if}
{if $form.f.relation_issue.errors}{$form.f.relation_issue.fieldErrors}{/if}
{$form.f.relation_type|unsafe}
{$form.f.relation_issue|unsafe}
{assign $prevField}
{foreach $form as $field}
{if strpos($field.name, 'relation_type') === 0}
{$field|unsafe}
{assign $prevField = $field}
{/if}
{if strpos($field.name, 'relation_issue') === 0}
{$field|unsafe}<br />
{if $prevField.errors}{$prevField.fieldErrors}{/if}
{if $field.errors}{$field.fieldErrors}{/if}
{/if}
{/foreach}
</td>
</tr>
<tr>
</p>{/if}
{if count($related_issues) > 0}
{foreach $related_issues as $verb => $rel_issues}
<p>
<strong>{blocktrans}This issue {$verb}{/blocktrans}</strong><br />
{foreach $rel_issues as $rel_issue}
<span class="label">
<a href="{url 'IDF_Views_Issue::view', array($project.shortname, $rel_issue.other_issue)}"
title="{$rel_issue.other_summary}">
class="label" title="{$rel_issue.other_summary}">
<strong>{$rel_issue.other_issue}</strong> - {$rel_issue.other_summary|shorten:30}
</a>
</span><br />
{/foreach}
</p>
{/foreach}
{/if}
</div>

Archive Download the corresponding diff file

Page rendered in 0.13157s using 14 queries.