aboutsummaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
authormkanat%bugzilla.org <>2007-09-08 05:14:25 +0000
committermkanat%bugzilla.org <>2007-09-08 05:14:25 +0000
commitaa888f2218179d59b4f0b8e51e43b863f1da3e43 (patch)
tree4e6bf6ff4b7066b19c5b1728dd325adf4bed1f78 /Bugzilla
parent16336299f17522d040736cb0f063694381d9d761 (diff)
downloadbugs-aa888f2218179d59b4f0b8e51e43b863f1da3e43.tar
bugs-aa888f2218179d59b4f0b8e51e43b863f1da3e43.tar.gz
bugs-aa888f2218179d59b4f0b8e51e43b863f1da3e43.tar.bz2
bugs-aa888f2218179d59b4f0b8e51e43b863f1da3e43.tar.xz
bugs-aa888f2218179d59b4f0b8e51e43b863f1da3e43.zip
Bug 287330: Multi-Select Custom Fields
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=LpSolit
Diffstat (limited to 'Bugzilla')
-rwxr-xr-xBugzilla/Bug.pm110
-rw-r--r--Bugzilla/Constants.pm2
-rw-r--r--Bugzilla/DB.pm18
-rw-r--r--Bugzilla/DB/Schema.pm12
-rw-r--r--Bugzilla/Field.pm21
5 files changed, 138 insertions, 25 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 7ed76311f..631c77caf 100755
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -70,6 +70,9 @@ use constant LIST_ORDER => ID_FIELD;
# This is a sub because it needs to call other subroutines.
sub DB_COLUMNS {
my $dbh = Bugzilla->dbh;
+ my @custom = Bugzilla->get_fields({ custom => 1, obsolete => 0});
+ @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} @custom;
+ my @custom_names = map {$_->name} @custom;
return qw(
alias
bug_file_loc
@@ -98,7 +101,7 @@ sub DB_COLUMNS {
'qa_contact AS qa_contact_id',
$dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
- Bugzilla->custom_field_names;
+ @custom_names;
}
use constant REQUIRED_CREATE_FIELDS => qw(
@@ -140,6 +143,9 @@ sub VALIDATORS {
if ($field->type == FIELD_TYPE_SINGLE_SELECT) {
$validator = \&_check_select_field;
}
+ elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ $validator = \&_check_multi_select_field;
+ }
else {
$validator = \&_check_freetext_field;
}
@@ -157,6 +163,9 @@ use constant UPDATE_VALIDATORS => {
};
sub UPDATE_COLUMNS {
+ my @custom = Bugzilla->get_fields({ custom => 1, obsolete => 0});
+ @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} @custom;
+ my @custom_names = map {$_->name} @custom;
my @columns = qw(
alias
cclist_accessible
@@ -175,7 +184,7 @@ sub UPDATE_COLUMNS {
short_desc
status_whiteboard
);
- push(@columns, Bugzilla->custom_field_names);
+ push(@columns, @custom_names);
return @columns;
};
@@ -303,14 +312,10 @@ sub create {
# These are not a fields in the bugs table, so we don't pass them to
# insert_create_data.
- my $cc_ids = $params->{cc};
- delete $params->{cc};
- my $groups = $params->{groups};
- delete $params->{groups};
- my $depends_on = $params->{dependson};
- delete $params->{dependson};
- my $blocked = $params->{blocked};
- delete $params->{blocked};
+ my $cc_ids = delete $params->{cc};
+ my $groups = delete $params->{groups};
+ my $depends_on = delete $params->{dependson};
+ my $blocked = delete $params->{blocked};
my ($comment, $privacy) = ($params->{comment}, $params->{commentprivacy});
delete $params->{comment};
delete $params->{commentprivacy};
@@ -322,9 +327,9 @@ sub create {
# We don't want the bug to appear in the system until it's correctly
# protected by groups.
- my $timestamp = $params->{creation_ts};
- delete $params->{creation_ts};
+ my $timestamp = delete $params->{creation_ts};
+ my $ms_values = $class->_extract_multi_selects($params);
my $bug = $class->insert_create_data($params);
# Add the group restrictions
@@ -372,6 +377,16 @@ sub create {
$sth_bug_time->execute($timestamp, $blocked_id);
}
+ # Insert the values into the multiselect value tables
+ foreach my $field (keys %$ms_values) {
+ $dbh->do("DELETE FROM bug_$field where bug_id = ?",
+ undef, $bug->bug_id);
+ foreach my $value ( @{$ms_values->{$field}} ) {
+ $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)",
+ undef, $bug->bug_id, $value);
+ }
+ }
+
$dbh->bz_commit_transaction();
# Because MySQL doesn't support transactions on the longdescs table,
@@ -469,6 +484,7 @@ sub update {
my $delta_ts = shift || $dbh->selectrow_array("SELECT NOW()");
$self->{delta_ts} = $delta_ts;
+ my $old_bug = $self->new($self->id);
my $changes = $self->SUPER::update(@_);
foreach my $comment (@{$self->{added_comments} || []}) {
@@ -480,6 +496,24 @@ sub update {
$self->bug_id, Bugzilla->user->id, $delta_ts, @values);
}
+ # Insert the values into the multiselect value tables
+ my @multi_selects = Bugzilla->get_fields(
+ { custom => 1, type => FIELD_TYPE_MULTI_SELECT, obsolete => 0 });
+ foreach my $field (@multi_selects) {
+ my $name = $field->name;
+ my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name);
+ if (scalar @$removed || scalar @$added) {
+ $changes->{$name} = [join(', ', @$removed), join(', ', @$added)];
+
+ $dbh->do("DELETE FROM bug_$name where bug_id = ?",
+ undef, $self->id);
+ foreach my $value (@{$self->$name}) {
+ $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)",
+ undef, $self->id, $value);
+ }
+ }
+ }
+
# Log bugs_activity items
# XXX Eventually, when bugs_activity is able to track the dupe_id,
# this code should go below the duplicates-table-updating code below.
@@ -504,6 +538,25 @@ sub update {
return $changes;
}
+# Used by create().
+# We need to handle multi-select fields differently than normal fields,
+# because they're arrays and don't go into the bugs table.
+sub _extract_multi_selects {
+ my ($invocant, $params) = @_;
+
+ my @multi_selects = Bugzilla->get_fields(
+ { custom => 1, type => FIELD_TYPE_MULTI_SELECT, obsolete => 0 });
+ my %ms_values;
+ foreach my $field (@multi_selects) {
+ my $name = $field->name;
+ if (exists $params->{$name}) {
+ my $array = delete($params->{$name}) || [];
+ $ms_values{$name} = $array;
+ }
+ }
+ return \%ms_values;
+}
+
# XXX Temporary hack until all of process_bug uses update().
sub update_cc {
my $self = shift;
@@ -1171,6 +1224,19 @@ sub _check_work_time {
return $_[0]->_check_time($_[1], 'work_time');
}
+# Custom Field Validators
+
+sub _check_multi_select_field {
+ my ($invocant, $values, $field) = @_;
+ return [] if !$values;
+ foreach my $value (@$values) {
+ $value = trim($value);
+ check_field($field, $value);
+ trick_taint($value);
+ }
+ return $values;
+}
+
sub _check_select_field {
my ($invocant, $value, $field) = @_;
$value = trim($value);
@@ -1233,6 +1299,9 @@ sub set_alias { $_[0]->set('alias', $_[1]); }
sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); }
sub set_custom_field {
my ($self, $field, $value) = @_;
+ if (ref $value eq 'ARRAY' && !$field->type == FIELD_TYPE_MULTI_SELECT) {
+ $value = $value->[0];
+ }
ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom;
$self->set($field->name, $value);
}
@@ -2670,6 +2739,9 @@ sub check_can_change_field {
# Return true if they haven't changed this field at all.
if ($oldvalue eq $newvalue) {
return 1;
+ } elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') {
+ my ($removed, $added) = diff_arrays($oldvalue, $newvalue);
+ return 1 if !scalar(@$removed) && !scalar(@$added);
} elsif (trim($oldvalue) eq trim($newvalue)) {
return 1;
# numeric fields need to be compared using ==
@@ -2941,11 +3013,19 @@ sub AUTOLOAD {
no strict 'refs';
*$AUTOLOAD = sub {
my $self = shift;
- if (defined $self->{$attr}) {
+
+ return $self->{$attr} if defined $self->{$attr};
+
+ $self->{_multi_selects} ||= [Bugzilla->get_fields(
+ {custom => 1, type => FIELD_TYPE_MULTI_SELECT })];
+ if ( grep($_->name eq $attr, @{$self->{_multi_selects}}) ) {
+ $self->{$attr} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT value FROM bug_$attr WHERE bug_id = ? ORDER BY value",
+ undef, $self->id);
return $self->{$attr};
- } else {
- return '';
}
+
+ return '';
};
goto &$AUTOLOAD;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index a5fbba61d..4406d7415 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -119,6 +119,7 @@ use File::Basename;
FIELD_TYPE_UNKNOWN
FIELD_TYPE_FREETEXT
FIELD_TYPE_SINGLE_SELECT
+ FIELD_TYPE_MULTI_SELECT
USAGE_MODE_BROWSER
USAGE_MODE_CMDLINE
@@ -340,6 +341,7 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib';
use constant FIELD_TYPE_UNKNOWN => 0;
use constant FIELD_TYPE_FREETEXT => 1;
use constant FIELD_TYPE_SINGLE_SELECT => 2;
+use constant FIELD_TYPE_MULTI_SELECT => 3;
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 7384e7b5f..4aad803c6 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -642,9 +642,8 @@ sub _bz_add_table_raw {
$self->do($_) foreach (@statements);
}
-sub bz_add_field_table {
- my ($self, $name) = @_;
- my $table_schema = $self->_bz_schema->FIELD_TABLE_SCHEMA;
+sub _bz_add_field_table {
+ my ($self, $name, $table_schema) = @_;
# We do nothing if the table already exists.
return if $self->bz_table_info($name);
my $indexes = $table_schema->{INDEXES};
@@ -659,6 +658,19 @@ sub bz_add_field_table {
$self->bz_add_table($name);
}
+sub bz_add_field_tables {
+ my ($self, $field) = @_;
+
+ $self->_bz_add_field_table($field->name,
+ $self->_bz_schema->FIELD_TABLE_SCHEMA);
+ if ( $field->type == FIELD_TYPE_MULTI_SELECT ) {
+ $self->_bz_add_field_table('bug_' . $field->name,
+ $self->_bz_schema->MULTI_SELECT_VALUE_TABLE);
+ }
+
+}
+
+
sub bz_drop_column {
my ($self, $table, $column) = @_;
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index f8c1588e4..1c6ee352e 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1254,6 +1254,18 @@ use constant FIELD_TABLE_SCHEMA => {
sortkey_idx => ['sortkey', 'value'],
],
};
+
+use constant MULTI_SELECT_VALUE_TABLE => {
+ FIELDS => [
+ bug_id => {TYPE => 'INT3', NOTNULL => 1},
+ value => {TYPE => 'varchar(64)', NOTNULL => 1},
+ ],
+ INDEXES => [
+ bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},
+ ],
+};
+
+
#--------------------------------------------------------------------------
=head1 METHODS
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 1830784a9..6555bba96 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -252,9 +252,9 @@ sub _check_sortkey {
sub _check_type {
my ($invocant, $type) = @_;
my $saved_type = $type;
- # FIELD_TYPE_SINGLE_SELECT here should be updated every time a new,
+ # The constant here should be updated every time a new,
# higher field type is added.
- (detaint_natural($type) && $type <= FIELD_TYPE_SINGLE_SELECT)
+ (detaint_natural($type) && $type <= FIELD_TYPE_MULTI_SELECT)
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
return $type;
}
@@ -454,13 +454,20 @@ sub create {
if ($field->custom) {
my $name = $field->name;
my $type = $field->type;
- # Create the database column that stores the data for this field.
- $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
+ if (SQL_DEFINITIONS->{$type}) {
+ # Create the database column that stores the data for this field.
+ $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
+ }
- if ($type == FIELD_TYPE_SINGLE_SELECT) {
+ if ($type == FIELD_TYPE_SINGLE_SELECT
+ || $type == FIELD_TYPE_MULTI_SELECT)
+ {
# Create the table that holds the legal values for this field.
- $dbh->bz_add_field_table($name);
- # And insert a default value of "---" into it.
+ $dbh->bz_add_field_tables($field);
+ }
+
+ if ($type == FIELD_TYPE_SINGLE_SELECT) {
+ # Insert a default value of "---" into the legal values table.
$dbh->do("INSERT INTO $name (value) VALUES ('---')");
}
}
.0?&dRfyw p+s0K"u Դʲ(qd 0k3W{ZӍĶ5)LNuȉzBw+j|@(TBMHg_'$$7u*l,ˣQ+J2Q>bJ \.)2x1]>hYzvq^X>-yuQ>b9hq9*찗wT>|g;()AXQf>U=dqkOF๨Q7(C[( 7}ߎaiqM4%1Q }X e=djVdvۙ#fŅLVd=pmbmfsWNKu_cp ! :>*V%IWMdjfUZm-w%lV##}-PntZ@G#ʅ 1HEPk/3sR׶bӲPd^jf}Ȏä½¹YLtFUXF>c6 }1d-ox/E\a=Da0*w1OҳtǤk L=^z]"ra\J i(RJQA@_@2gB8B^{esg9r;4z/TܥU_-CdG9e*#ciD\*9چ;nO87Y#OP߬:EPCTl;ud#3Ю>`ɝ шL^ <{ S˜^+BJ16~o 6:, X`P "X0$eahI<>0yhgyn(|bm"y)uZx,w#BDx{D5Y';? Zym)6;oUc~Nd¯x #q;2_(۸dUXn@8=;0OwAʴFQb.yK݄I!m+Bq)0dM_ΧyGQ ꩻ MMWqsXtql~=ew'$#(*T٪J )jzs8y=:8;m`5xN]z9j!NU!n^ˬv%J)] %m=ͣwݚ ƷA1 TU~bU<ȅض\*xj0R)cpJ XL'6XٱG6>5X;֪As;Տ \fTzm3ބ&/_$U-R(pbrDǵ2=B2~r 3(f>]}"2jWK*1dc͋*^ӣa"D(Ҟ\K[FoD?[X(I˿(`{{wۧ ė6QYFnqo/AJ8AjA-AkKXK)OsH(U*|p4UBZ͵0eW6Hr BቛUF(8V(=}@#`!L(a'9ܢ(Ȟsˆ-xWfgH TT-fM8~m-$Z&F)$lu6AVPYүbСIs_WbuP; 8osԧo%T]ǛR]d&^&=Ux$mҼԆ!JJutW8.}%?SWv,|fOWڜ"-w"jf?q'WcPo 6HxB[fH KY[ Ch4׽^6ր6x1R5M4O_ptD5}LCܒ\`6C[~2&F8 3#20\# }꠽a-„tTQB;2lRBn]"iL"(dCǼ-և {x|=ظf) ^I2s#z tP?uLʣGɈF_HJa&bFn"Q-fem.N`](-==9?.4c/ʯj$-ێ+%CPŀP鑸c'%KWj LVhɉKF]*҅.C݊ΨF9ߕכ@vEu2[륙\WIKThL۳EPAW, 5p1r9u0Pq:J 3-caݟv0l<-{ϙZ4>mV!6KGK-17!ʾoh-G>M r?i:bE:LN\cq@EO4<ͭwۏ4Vg @O\9#M4ְ4M+5f!2+̘W2#%b,Z2V Wpw$oLBL;uIwHpتXM5A [`z0rjt))8 E)XYhW8Apn-)b9FRp1^Z;";Ba^l99 aeQ~o"xQԕ@ ڢ ]t\-9}F9Wru lo *`l>EřDzCQRQʶ ) ɠ}ӄ( o)!҉ح>E95at^ V`@*+U~Xy`hL*X#{)$zֵI=Lzc=ҽ:%ƅa&,2*em1?բγknT&]OZxKn(9bֈenG έ%h(L)AHWZP4El`['{Ƚ1syTY_)N>_-<0 Wqz@%w+ERE1`x9.zJ1р;K瓄s} "\*־?xnXWO=(ʗKIK6Qtj'ZDG3;"-ij:?1J%?JB`p.f)q8xˤ6Z6 jamrpHnDg!l>4ڥ#_D!rzMCX3#׭bN-fHK&9 YGi,/gW\oT@EԪRvLodrԶ61j_G ;&OڷC Z?@:B})N>n}?#[M$؆BXqh%!B}'K":ӆ޴;gmT\ j p]4mYBoZ.q%Q[Ls< ֍T/3~~a쀆LUFC ( LQU[Y:[BI=mI6z9ʜ8{52kmmKe4[gW,ˊ`4H2nQgԹc|aFaC A%0d %{L̉197c9jn%{7FIy/4,!{TǙB5c.d`T sOQɕ,*KS{ϲj 9I4P_-)&$L-+#MgGe 90'lt 0Eo6<@"ձw+i^ӧ-Z첏/' z63`€ ,lPHIDq@'kAz-pҰy-H43]2 54ycOAo wNhqVSc k2 G4 cD+u.k%[ JtE=HK7T8#{4R2b!n%OMGtu=1K᪕S>7/oEqh5zаkӸ[H/;qf-2J'(" >Y+SdE.ޕ* #GmQNk w~rjTm׈=AYd娦1-FsK_.(Q<&,7kEbI7T6sz&!F|s1n-Y jۯ0g}_Ma A,r^s┅e1(G㍬U/䘰ܩco %3wtzx~I?B3ZΊxB)H꭯Tp"tz+*3}Xد #ѳT?#>̡lŦDtLD8,L(yɺ]#jlqxvoz3wL[k !xX&xiVq"sh@xџO ւT`98O|(%48iR_b 709M3bwl_& o)Xe l '_md07E_5yoε1, ;: CZ??U9@1I  OpZtKȀxk0 adohfI7%mdv5fOIsjز0PI̾F%y(G:ɒn=%ͼfo >Bg>O:+׎"z+R S|[9=*$2΃ 3ygx@WivЦ:S&,4Ե WSeoԩTCi wJDf#* 5`?]c/Ö+b13+i0{CMD臰lPqJ] 3kreT$4Bu~/Rk~t ^#療T ;y"K/-N|CbB꾎paGذ[{m1(0*yڷFw-E$w':?pf,,'pg Fg@G,\A_[\i>7ӒU}X6Qˠ8V[`8x4isbD JU`lulH&EaуӝǏ.C,N_'1$ fyߋ}H&^Z}\?UET2,<.uᐜ˚Rvvr%=NKN(͕W >9VEg_P$&ua\=U}).k7E4/ M+eP!t }Q)$05%$=RsѨ dވdއ'm Ekx m^36%{D:؜k0v~ѯ0tfqJ QD(5Z|AoKy`kPV[$T4;*%#b;v6jJ(v 8uyHftl %}E븣?jݽvT×CĽ(vhs3n~~PgRlC]@udU^rA|jN-RJq>HnK4}&xl"Dw2;Lm ~?qM›19./Tq`=&[ 㩊8iT|D1Of[[ :cms=̹K{~x<+'pAV:Đf3ӦZqDYq\150cn*Xi /Ę(x:N4Xx-d=zz ;$׭ٜ^g;(ٔGZMD$UǹS5ެ8:accqjPqb2JAJԦ|W9g a@>L+1Vh>y;&x6  7cN4]gHè숥d|AR{3ly.GJE&w +8^ݢl W~-yKccE>ɋ[X}:@+T0A|?)5w.k,DU_eqD]Y7v^fEؐ$5@n'?DhYE# S,T-4 I<ᗫDQ 2:+'L _njK]%k[CxTU6,b{Ӥ˕/#Z% }pU (y7g8Vi `7^JH} L?u#=VLg6#4~aq!Я'*?,,OB AR6jl .sR ڭXR5+9F}fsy9`B> ON7َj{_O?w'5pcu 'vh-S͹ypaIu@d 5NauݸT= Ĝ oͨVJ};NwO6rz&˩J$pׯ:Re`K 8TREಯ?TQ%eVRگX+i(c} o|?IzTq3Ü)ۥFWVwC)fIetY[鵱vf+^EMeEܓ&@Pn#>ev] R"Kr(KEYhj>V;,,W -]Bq]Mt%Tah+7\1r).*I),KS]BxvBXGs Aﬧց8QC48k.H:Z&IUb)oҕbRB)i >:<5 w[ҝ= ; \S1VU= GFqUXtX E!;a٦WU"AŹh B-a0]Q-3yݨXvwyiu{Sz yژfd^t8vw&mRY LCzڟf"tݥsҊ<"8\q8zMǍ!/^ye }lm>MYd$d,C XkYGјP9=8ϖZ#80mNӒ| RlYÚc01r3rep@ 7ON$-$ܓO6<*-=y'ύ3}OUW[  o&n__ Lbޢ ,s>uGeiSFoYzl{W mĮߤs媞} S?l='+sz Lc[b׉/J&ڟˬ>t.j6X*2xAݶDoq.w/1S$ҵ0t)s1q҄ *ŀM./ä- yuQݢ?m-[;%4 &a({u 5x{G-O_qNŁkeϒ6)ɺWN!7}_ߞWGA|Ɣ2QjM!Xr38toՇW FX `(4&X;"& Lm#݊M_iwwM.J32qA]4w, AHR [tB qςvM"TM0n$/4Ptt9̡ު L XE[F̹U՛oq[`M2Aʇ#5Pഠ黤^ӵstp^Ww `ǘoOHSL0[ WÌ r\Gbˢ'&aGE/֠WY6}~OE Ky: D1' -#Ah3{)ePC'D#>U?(H9gk2w:4i9HeD~fšrnT"Kn;E*8"dLKop̈Nr  SbPIJMbpP 6ӎ~MA=U17l&*r- hqTc!#Z9O y"Y]7^1ONэ )r2A1a\Ҭe:' a1)fc7M nFJ#28d caG-u\)Eh]A]"qe<~F;3Dw\c",Q227iq9DQn6K6ԿHKj#cD7w 48%X<"L^ qbr 2]]բ`2|quLV]ѕ c96h8B1x }l5bcS`sBUtSm1Eco*3rOs/-ڼ rᆈ!IeWYD]. ?wg2wMTw_y:JgF%Xvn~Y!5nߌ>uylZYX pP\+Eq[$AuKȑݻ(1MF|?lU4tXmeU}Soą J_Jd8Rv7Q%^NAL.=xung%}n5-6i ֻDz9~8ĨG=T(16x}ojkoLlJ/[}QGT8RwV%CfH!ih9nj_~ ׿yOjcS$ǯi 3F ]DSS_.syNи""&ni ;8wcJ9Kg |/n!BeJ卦1DOA+lv3&Mr_Tr!^Hx)/[17r^?(+Yϻ)F_Z"NA5 F<ܡ88qs  "w&?bqQ0xg҈P_LvA"sBj;l͹_>pn֑IH,*LPCByǷ1,6CL=x+I{ 4bCsk$"ġsۯ *yw$"|*qn'߬󟑿*c<4S]i@PX?<D/$;7)sܘQWܾg] LL;/ ؂;ܳ$xe) #un!Cob0+ }U>xI6ŌV&tbrt]AkSotMdf *Q29.\Ӝ[!G*B"j-/>#Z[&oʫ`THh X9>[h)g}^^'뽋 / rM1z@_ﯿ+$f$E_iQX}2hMܸ /]U।SW' TTG[I\eaӒ>ԗ^PuīK4D>mP*9߆&BoŃTVXFi;΂sI\Kv.̲.ՄOTDUjm~@t:SZ}ݥ'GRX.U\2 CAKhG~4S'خ rЪðhLOfyM?0qRjdW媒mi!DR&=馤_Y=@X 97+:| quD4t  K5i&:"_Fku􋅨$eUzS/`d.Ts /mME&R6+J {N:%ID0XVAM4 sfʃFV+&W -DF/L`JbbY#Pz.mTaY0=Su J``GL%f*yW6 qFSFN(GmZoJcD VT]*C"=KgLe%<ʡK ( Ό8ɅU0J-yG"񮶚7Pg`g~Yw=>GSE5 M4&!ą*Se6V#e hHD%[1}BǦ],-'1^PUPNUNIu"TDjI/-b=-mZ `t]~RN g}Oebrc'.TbvֆP oV"]Z\ȓ̇b:{u`)yK8'\$o|IplRq[aALb-?~NuG3g)| \ ҞIDn'B&_Thus9&~0"[h }sa"9ָۃBʼ7> XQ\5 f0Qqߥ0*IZDȢ7B\"&uqԄ0ۤjti/Zr4ԝv;`шFi/Y90b^Cd|W6~s~0E脭}V8]Sh^[ D>6Vpyn{ ,v"/]0 1T$Dɒ nÍh5qtqᯍF,f!8KU\]Epe?˕z!L_BYA/P<]|m'"kȰNtȋ)]QĽI3t1䨒bz928.B~$$[tYz>X, AzF?yt=Ek8C'3sDq#\ĜD2-Y,w b:U§hnC#dFA2Z'F vEt+?e;#yĩ0u`rHIwl|'OJcjag]z|m+_,_WɁ8m!#;BR"sќTԑB]"֢Z8eX7R]7L)lQAuDMS!ֵ$J ܄ސ.g^N%DĚ`9X(ru& [NCYmr]5uk̼2G8CVci88{D4=a dwPhKwqɝyO]i )d=gS xGNJ5Юymg8Y qeĆ-R80*DL~PI4I.f 1 $#Єsf+Ȝ 5O]XªH :Q'.| Ua^sQ>RK/~=Je|ZD`9㜮GR& f!OE.zN'g˃yoNaW/jbFPko]V}ZfBϵi:OD}uphjgi8bJ&ɢ(VplDg{G~CD&xQTZ=3ּ\C(8};pC\Px}9iqBQ{kLx;QD}V=užpE@sSg0UB'u;>Wo>BCB",$-޽wkIYš:|ǵ8c.ܨC2RqϘyaynjF0Ոvi` N ;3 k-?S$7M0#.8JƊ$}xg/8dYO7B~rM:RYLyq- pQ} IfŐgԅbˣUv|x.J^Q{G0>#f15%(Ft9),DYaw)&jPQ2 i E;&0O[aKe7~'Q[ ޕ߼ oK3C-ܹoN Ag*#L3]7 QNMAj8mGuD傲_drηF ¯\Sh}bϩb׽\e?cQܠ\Qzvɏ?|1i-4̦9꠵`7>750W݃3Q;r)784a,@j $+ XOyٚ|h "ķăGSRꄨ L]w:]bXQN p 9o.Ih#'9È$ԍnI9zuO&yJ.+Z5xwiawedkUN>Ot; (&(ݏPI7B B?e }Qv[jqobc{ݗ|cO]^.Ƌ̉_8K-G*~%i 2sJjx2, GSv`aV Vv(Z5']];r~Md(9hT4 khZK"35K[:kBs14c/;Т3ʿ= a.Z߂u8hn˴i0~{)peF,#JjAsHj挱bޢTf~ސTj 7WHz̝(Bve1x=fA <ϓI@B vXktن׌0o;FkM ^__}cS $neYpP Z& bۇ~Cp󘀪;$>h2|(A3¼]@^{oׅٝ T hﺚ# PhP,YlA!>q3hKt5)qYeQ'ie!>w`Y_k#[UX?2*3 ccBQ3 45 C(ԸeXÞkgfDl9V{X{: opIFy<<Df QQg1FPA Iͨ9t`a;HEa 3š+[F3i ~^h8_Fc&WV;B֜6vPJ)F!gt)'e C@] {v!7"ɭ"W޹y.''8@ Axg $nMT97u`FL,q)aV9=eϚRV{*' 1T>\n/_vOUrk@1lWӝksK~{ʹЋl~ЦuZ[Nչ;.ҲAMqe~ {1.エřW*oX&*3{|broP~ 6VyܱTE7͂ZxvX9uߥ{5pF-{c]x<}arēV=")11Mvfymo6j*I—46{&U3}g.Iij Z^ox%i"Nr])veLcӻ-hq^6 I'*VM p[wă̳jF 6ŊH e^KBpFEN;(0/gJ5E;Iȓ込;X[\h Tƫ(b:I C,JN(`[C{QvgVYjr5mr(KSVq;4 jk$NW}0p֠LJTh}Zٱ-QQ|ӵZ`B>`悥J_qff +_.1a+ -n?R D?$Y! 8ު6!+n/Rb ` Fyn&kd@knpQaKPJ/5ng}t珼^5Py)>6ǟfmb_I@q)9m輳7z{59; Giz+nwP%'c,xhy.0^Ct;5'禑pbRS#t<`a56Y ]wc8:Q™1)kotYs0iACT3І-Ҝ/5\\W|ڴ+iQ/-l%yctL%||d76KoL66=FսDzCv'Љ{%8wA-Yρ*zR蜖h D8X&AU,^d%#JTbL)w\FctZxjmb |FzYrE3tD0ba!mWgM i˃ H:/N@gmF4-2rn gW,AdQ 3A`\ݷ,XYe0I_8 g_\.rcs 29zbfh 0XHgѳPk/*i&cpnؑQ Em7=1f'RwklA JY} sUp`6|Aޢjի04Gm,i&葛l[P ?@VP*F::=wK&MzV>I?aAPyUXs] N&($L5|c&-{#ӢAc}I#큳E`es* Zi#_gH&~GF3Cܓ:uU#"l ~_\Ԉn' (EzhJɛtt%_ʰb䤙 Ad5XT*S U=/%!=58Jb'8臫PL@uYoн`.y]Ҹ WW5Y^; ΛqPRE` LGёBu!T/;ݐ}M'| C$RYq3lSh Ϡy+wVxv.ճ=#f/Q`\^h$QD7)<һ=К%E{`'@/ͱX+T16vRzC]zQQt3gTGz?2饳&|\RON]v;k\Ee3&yMٲKc,/4d8>ģW<7kv/1Ur|FE˥!0G_ c=>q&tRs=[ORHX=##`F %Qe*cj#X?O19֏_&PF}`Gҷ?(J ֎V(Bkh*-5D;s}-- >w#h趫?"1hה1r3`b0oAs L;Nx2g>lVL bD~s^C`Γ `^LӼBB#:vBѽ7(0£JLE@po=M96 r]_Аm 2Boh_\O p)$K>.5@6T\lQZ SLڔt ?GMj%~Q(`s*QL q'U{zߐW93rNWMp7L^"QNh1% M'8,Ίܡ.E8^>:q% _4!啳@\\T]atjj'y-~W9a_?]ГV ,9Ψ |.kՕHx$asˡ}kEhmܕ(84E<yrZ7 |!Ϡ`'\ <2|rfC J*` 1=hgVw爍.0F>#lSS'ܪ.eBN )ǼVJJ:怏0v/a+ɐ3=^+_y<՚@n;]2>pz1;4maR<0<&9$|AĮk+̟w 'w|!.:^TGiXh$ ?ySg|R(MDՃl?Pbƻ9e]2Fe 0:I` pQ7eQް lwq{U.F(qz }^` `ʥ[1aɖYN;PXOn(70fAY1ٱ:Od)c/'[SI:!o ɓC)mi8 ]=0%~Jf_uʵx0g5:Q 5\ 6x]U5ޓ_j83ʥR&_S l3=LҪz~ij(nưwN~.T`XwKOC t1j`]"YYX%Ȣ}PԃkLC /K#rq2=i'<̖6oOLvgHݞi-h_K@F(!GjȵiL. Gն/#d4qYC9en nC]\.$EEiIa^S8\][g䲟%&=M;\HH[p3 Ndo`Hœ_ܭRm WhWS, D ҈`LGe>%@R'ē(R_ō;-!ouzHAwp-?. 6CT)`ζBM[c gwA4吃{L/ h26b]U$,O=&? =馠?u F){MHd :. ȃkwi4llCkLU͹c\F +1I6c6U1`]GδYn1q08ЪDӀxز'7K',j Ν`J/ퟩ8#F2IN;sZXd\aIAOTt77.~V5̶ ԥ.Bww&xAo>:HJbDN$2bpEbb'&mb|ص|/.ssw)Ή2g{9!EHej;]0+(x:ShN/w%;bK}جujAVlGޢnw.$#%tXD5pnbQ(APsnwEc$Cn&@ vݪ٘Mm{'m6z_b2E) `RchY M"f \ţ{\jWMy!30 gl"gQ9ۓbޗ8C3bb 3ygks悷OVƻ6cʧu&"!S,`;nmޝL`cr&k\(/ܘg+Ud^k1{}4z~#A3aCn4 4΃ |b5bgٺ阬u-m9A[ȹ*dTq'Q󣫌=iu{x @ xS$.iKhYvpR\hZ{2N"K[@]ՋٱmV<W9MvMc/#gfk v=nB"O2_+eZ{模-Ykk٦Ǡ#6:,] Yr`B4O7eS1.B4:vqk _afESzoAlĥg.bW!Z(!#XVuFG!%u۱-C& H/A(