From 6d892359c7850f4e0becf6b06e12706077e17d4a Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 10 Apr 2015 14:33:16 -0700 Subject: [PATCH 1/8] Private clans UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add private checkbox for clan creation. Add info popover for private clans. Subscribe prompt for create/join if necessary. Don’t list private clans in public list. --- app/schemas/models/clan.schema.coffee | 2 +- app/styles/clans/clans.sass | 3 +++ app/templates/clans/clan-details.jade | 4 ++- app/templates/clans/clans.jade | 6 +++++ app/views/clans/ClanDetailsView.coffee | 9 ++++--- app/views/clans/ClansView.coffee | 35 +++++++++++++++++++++++--- 6 files changed, 51 insertions(+), 8 deletions(-) diff --git a/app/schemas/models/clan.schema.coffee b/app/schemas/models/clan.schema.coffee index a7d646821..47c1e481a 100644 --- a/app/schemas/models/clan.schema.coffee +++ b/app/schemas/models/clan.schema.coffee @@ -7,7 +7,7 @@ _.extend ClanSchema.properties, description: {type: 'string'} members: c.array {title: 'Members'}, c.objectId() ownerID: c.objectId() - type: {type: 'string', 'enum': ['public']} + type: {type: 'string', 'enum': ['public', 'private']} c.extendBasicProperties ClanSchema, 'Clan' diff --git a/app/styles/clans/clans.sass b/app/styles/clans/clans.sass index 55e7d501d..2ae06af77 100644 --- a/app/styles/clans/clans.sass +++ b/app/styles/clans/clans.sass @@ -6,3 +6,6 @@ .create-clan-description width: 50% + + .popover + max-width: 100% diff --git a/app/templates/clans/clan-details.jade b/app/templates/clans/clan-details.jade index a23a67b96..6484dd947 100644 --- a/app/templates/clans/clan-details.jade +++ b/app/templates/clans/clan-details.jade @@ -3,7 +3,9 @@ extends /templates/base block content if clan - h1= clan.get('name') + h1 #{clan.get('name')} + if clan.get('type') === 'private' + small (private) if clan.get('description') .clan-description each line in clan.get('description').split('\n') diff --git a/app/templates/clans/clans.jade b/app/templates/clans/clans.jade index 5d87b4960..89fbbc9e0 100644 --- a/app/templates/clans/clans.jade +++ b/app/templates/clans/clans.jade @@ -6,6 +6,12 @@ block content input.create-clan-name(type='text' placeholder='New clan name') p textarea.create-clan-description(rows=2, placeholder='New clan description') + p + input(type='checkbox').private-clan-checkbox + span.spl Private + span.spl ( + a.private-more-info more info + span ) p button.btn.btn-success.create-clan-btn Create New Clan diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee index da3640f9b..57b80b5ad 100644 --- a/app/views/clans/ClanDetailsView.coffee +++ b/app/views/clans/ClanDetailsView.coffee @@ -1,15 +1,16 @@ -app = require 'core/application' -AuthModal = require 'views/core/AuthModal' RootView = require 'views/core/RootView' template = require 'templates/clans/clan-details' +app = require 'core/application' +AuthModal = require 'views/core/AuthModal' CocoCollection = require 'collections/CocoCollection' Clan = require 'models/Clan' EarnedAchievement = require 'models/EarnedAchievement' LevelSession = require 'models/LevelSession' +SubscribeModal = require 'views/core/SubscribeModal' ThangType = require 'models/ThangType' User = require 'models/User' -# TODO: Message for clan not found +# TODO: Add message for clan not found # TODO: join/leave mostly duped from clans view module.exports = class ClanDetailsView extends RootView @@ -135,6 +136,8 @@ module.exports = class ClanDetailsView extends RootView onJoinClan: (e) -> return @openModalView(new AuthModal()) if me.isAnonymous() + return unless @clan.loaded + return @openModalView new SubscribeModal() if @clan.get('type') is 'private' and not me.isPremium() options = url: "/db/clan/#{@clanID}/join" method: 'PUT' diff --git a/app/views/clans/ClansView.coffee b/app/views/clans/ClansView.coffee index 5936ead1f..fc7132b33 100644 --- a/app/views/clans/ClansView.coffee +++ b/app/views/clans/ClansView.coffee @@ -4,6 +4,7 @@ RootView = require 'views/core/RootView' template = require 'templates/clans/clans' CocoCollection = require 'collections/CocoCollection' Clan = require 'models/Clan' +SubscribeModal = require 'views/core/SubscribeModal' # TODO: Waiting for async messages # TODO: Invalid clan name message @@ -28,11 +29,15 @@ module.exports = class MainAdminView extends RootView getRenderData: -> context = super() context.idNameMap = @idNameMap - context.publicClans = @publicClans.models + context.publicClans = _.filter(@publicClans.models, (clan) -> clan.get('type') is 'public') context.myClans = @myClans.models context.myClanIDs = me.get('clans') ? [] context + afterRender: -> + super() + @setupPrivateInfoPopover() + initData: -> @idNameMap = {} @@ -54,20 +59,44 @@ module.exports = class MainAdminView extends RootView @listenTo me, 'sync', => @render?() refreshNames: (clans) -> + clanIDs = _.filter(clans, (clan) -> clan.get('type') is 'public') + clanIDs = _.map(clans, (clan) -> clan.get('ownerID')) options = url: '/db/user/-/names' method: 'POST' - data: {ids: _.map(clans, (clan) -> clan.get('ownerID'))} + data: {ids: clanIDs} success: (models, response, options) => @idNameMap[userID] = models[userID].name for userID of models @render?() @supermodel.addRequestResource('user_names', options, 0).load() + setupPrivateInfoPopover: -> + popoverTitle = 'Private Clans' + popoverContent = "

Additional features:" + popoverContent += "

" + popoverContent += "

" + popoverContent += "

*A CodeCombat subscription is required to create or join private Clans.

" + @$el.find('.private-more-info').popover( + animation: true + html: true + placement: 'right' + trigger: 'hover' + title: popoverTitle + content: popoverContent + container: @$el + ) + onClickCreateClan: (e) -> return @openModalView(new AuthModal()) if me.isAnonymous() + clanType = if $('.private-clan-checkbox').prop('checked') then 'private' else 'public' + return @openModalView new SubscribeModal() if clanType is 'private' and not me.isPremium() if name = $('.create-clan-name').val() clan = new Clan() - clan.set 'type', 'public' + clan.set 'type', clanType clan.set 'name', name clan.set 'description', description if description = $('.create-clan-description').val() clan.save {}, From bc35a27750dd2f3241d34bde6fec4803d96db786 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Fri, 10 Apr 2015 16:04:36 -0700 Subject: [PATCH 2/8] Private clans server updates Only return private clans in lists to owners. Get for specific private clan still allowed. Restrict create/join private clan to premium users. --- server/clans/clan_handler.coffee | 19 +- server/users/user_handler.coffee | 4 +- test/server/functional/clan.spec.coffee | 594 +++++++++++++++--------- 3 files changed, 383 insertions(+), 234 deletions(-) diff --git a/server/clans/clan_handler.coffee b/server/clans/clan_handler.coffee index d0f221518..45cef76ff 100644 --- a/server/clans/clan_handler.coffee +++ b/server/clans/clan_handler.coffee @@ -17,13 +17,15 @@ ClanHandler = class ClanHandler extends Handler hasAccess: (req) -> return true if req.method in ['GET'] - return true if req.user? and not req.user.isAnonymous() + return false unless req.user? + return false if req.user.isAnonymous() + return true if req.body.type is 'public' or req.user.isPremium() false hasAccessToDocument: (req, document, method=null) -> return false unless document? - method = (method or req.method).toLowerCase() return true if req.user?.isAdmin() + method = (method or req.method).toLowerCase() return true if method is 'get' return true if document.get('ownerID')?.equals req.user._id false @@ -64,12 +66,17 @@ ClanHandler = class ClanHandler extends Handler clanID = mongoose.Types.ObjectId(clanID) catch err return @sendNotFoundError(res, err) - Clan.update {_id: clanID}, {$addToSet: {members: req.user._id}}, (err) => + Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - User.update {_id: req.user._id}, {$addToSet: {clans: clanID}}, (err) => + return @sendDatabaseError(res, err) unless clan + return @sendDatabaseError(res, err) unless clanType = clan.get('type') + return @sendForbiddenError(res) unless clanType is 'public' or req.user.isPremium() + Clan.update {_id: clanID}, {$addToSet: {members: req.user._id}}, (err) => return @sendDatabaseError(res, err) if err - @sendSuccess(res) - AnalyticsLogEvent.logEvent req.user, 'Clan joined', clanID: clanID, type: 'public' + User.update {_id: req.user._id}, {$addToSet: {clans: clanID}}, (err) => + return @sendDatabaseError(res, err) if err + @sendSuccess(res) + AnalyticsLogEvent.logEvent req.user, 'Clan joined', clanID: clanID, type: clanType leaveClan: (req, res, clanID) -> return @sendForbiddenError(res) unless req.user? and not req.user.isAnonymous() diff --git a/server/users/user_handler.coffee b/server/users/user_handler.coffee index 13601fe9c..9549fdf0a 100644 --- a/server/users/user_handler.coffee +++ b/server/users/user_handler.coffee @@ -543,7 +543,9 @@ UserHandler = class UserHandler extends Handler @getDocumentForIdOrSlug userIDOrSlug, (err, user) => return @sendNotFoundError(res) if not user clanIDs = user.get('clans') ? [] - Clan.find {_id: {$in: clanIDs}}, (err, documents) => + query = {$and: [{_id: {$in: clanIDs}}]} + query['$and'].push {type: 'public'} unless req.user.id is user.id + Clan.find query, (err, documents) => return @sendDatabaseError(res, err) if err @sendSuccess(res, documents) diff --git a/test/server/functional/clan.spec.coffee b/test/server/functional/clan.spec.coffee index 92f6d5522..43ac49d42 100644 --- a/test/server/functional/clan.spec.coffee +++ b/test/server/functional/clan.spec.coffee @@ -6,6 +6,7 @@ mongoose = require 'mongoose' describe 'Clans', -> stripe = require('stripe')(config.stripe.secretKey) clanURL = getURL('/db/clan') + userURL = getURL('/db/user') clanCount = 0 createClanName = (name) -> name + clanCount++ @@ -41,269 +42,408 @@ describe 'Clans', -> throw err if err done() - it 'Create clan', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', 'test description', (clan) -> - done() + describe 'Public', -> - it 'Anonymous create clan 401', (done) -> - logoutUser -> - requestBody = - type: 'public' - name: createClanName 'myclan' - request.post {uri: clanURL, json: requestBody }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(401) - done() + it 'Create clan', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', 'test description', (clan) -> + done() - it 'Create clan missing type 422', (done) -> - loginNewUser (user1) -> - requestBody = - name: createClanName 'myclan' - request.post {uri: clanURL, json: requestBody }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(422) - done() + it 'Anonymous create clan 401', (done) -> + logoutUser -> + requestBody = + type: 'public' + name: createClanName 'myclan' + request.post {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(401) + done() - it 'Create clan missing name 422', (done) -> - loginNewUser (user1) -> - requestBody = - type: 'public' - request.post {uri: clanURL, json: requestBody }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(422) - done() + it 'Create clan missing type 403', (done) -> + loginNewUser (user1) -> + requestBody = + name: createClanName 'myclan' + request.post {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() - it 'Get public clans', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - createClan user1, 'public', 'the second clan', (clan2) -> - request.get {uri: "#{clanURL}/-/public" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - expect(body.length).toBeGreaterThan(1) - done() + it 'Create clan missing name 422', (done) -> + loginNewUser (user1) -> + requestBody = + type: 'public' + request.post {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(422) + done() - it 'Get public clans anonymous', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - createClan user1, 'public', null, (clan2) -> - logoutUser -> + it 'Get public clans', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + createClan user1, 'public', 'the second clan', (clan2) -> request.get {uri: "#{clanURL}/-/public" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) expect(body.length).toBeGreaterThan(1) done() - it 'Join clan', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - Clan.findById clan1.id, (err, clan1) -> - expect(err).toBeNull() - expect(clan1.get('members')?.length).toEqual(2) - expect(_.find clan1.get('members'), (memberID) -> user2._id.equals memberID).toBeDefined() - User.findById user2.id, (err, user2) -> - expect(err).toBeNull() - expect(user2.get('clans')?.length).toBeGreaterThan(0) - expect(_.find user2.get('clans'), (clanID) -> clan1._id.equals clanID).toBeDefined() - done() - - it 'Join invalid clan 404', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/1234/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(404) - done() - - it 'Join clan anonymous 401', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - logoutUser -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(401) - done() - - it 'Join clan twice 200', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - Clan.findById clan1.id, (err, clan1) -> - expect(err).toBeNull() - expect(_.find clan1.get('members'), (memberID) -> memberID.equals user2.id).toBeDefined() - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + it 'Get public clans anonymous', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + createClan user1, 'public', null, (clan2) -> + logoutUser -> + request.get {uri: "#{clanURL}/-/public" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) + expect(body.length).toBeGreaterThan(1) done() - it 'Leave clan', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', 'do not stay too long', (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) + it 'Join clan', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + Clan.findById clan1.id, (err, clan1) -> + expect(err).toBeNull() + expect(clan1.get('members')?.length).toEqual(2) + expect(_.find clan1.get('members'), (memberID) -> user2._id.equals memberID).toBeDefined() + User.findById user2.id, (err, user2) -> + expect(err).toBeNull() + expect(user2.get('clans')?.length).toBeGreaterThan(0) + expect(_.find user2.get('clans'), (clanID) -> clan1._id.equals clanID).toBeDefined() + done() + + it 'Join invalid clan 404', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/1234/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(404) + done() + + it 'Join clan anonymous 401', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + logoutUser -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(401) + done() + + it 'Join clan twice 200', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + Clan.findById clan1.id, (err, clan1) -> + expect(err).toBeNull() + expect(_.find clan1.get('members'), (memberID) -> memberID.equals user2.id).toBeDefined() + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + done() + + it 'Leave clan', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', 'do not stay too long', (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + request.put {uri: "#{clanURL}/#{clan1.id}/leave" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + Clan.findById clan1.id, (err, clan1) -> + expect(err).toBeNull() + expect(_.find clan1.get('members'), (memberID) -> memberID.equals user2.id).toBeUndefined() + User.findById user2.id, (err, user2) -> + expect(err).toBeNull() + expect(user2.get('clans').length).toEqual(0) + done() + + it 'Leave clan not member 200', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> request.put {uri: "#{clanURL}/#{clan1.id}/leave" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(200) Clan.findById clan1.id, (err, clan1) -> expect(err).toBeNull() expect(_.find clan1.get('members'), (memberID) -> memberID.equals user2.id).toBeUndefined() - User.findById user2.id, (err, user2) -> - expect(err).toBeNull() - expect(user2.get('clans').length).toEqual(0) - done() - - it 'Leave clan not member 200', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/leave" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - Clan.findById clan1.id, (err, clan1) -> - expect(err).toBeNull() - expect(_.find clan1.get('members'), (memberID) -> memberID.equals user2.id).toBeUndefined() - done() - - it 'Leave owned clan 403', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - request.put {uri: "#{clanURL}/#{clan1.id}/leave" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(403) - done() - - it 'Remove member', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - loginUser user1, (user1) -> - request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - Clan.findById clan1.id, (err, clan1) -> - expect(err).toBeNull() - expect(clan1.get('members').length).toEqual(1) - expect(clan1.get('members')[0]).toEqual(user1.get('_id')) - User.findById user2.id, (err, user2) -> - expect(err).toBeNull() - expect(user2.get('clans').length).toEqual(0) - done() - - it 'Remove non-member 200', (done) -> - loginNewUser (user2) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - Clan.findById clan1.id, (err, clan1) -> - expect(err).toBeNull() - expect(clan1.get('members').length).toEqual(1) - expect(clan1.get('members')[0]).toEqual(user1.get('_id')) - done() - - it 'Remove invalid memberID 404', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - request.put {uri: "#{clanURL}/#{clan1.id}/remove/123" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(404) - done() - - it 'Remove member, not in clan 403', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(200) - loginNewUser (user3) -> - request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(403) done() - it 'Remove member, not the owner 403', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - loginNewUser (user2) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + it 'Leave owned clan 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + request.put {uri: "#{clanURL}/#{clan1.id}/leave" }, (err, res, body) -> expect(err).toBeNull() - expect(res.statusCode).toBe(200) - loginNewUser (user3) -> - request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(res.statusCode).toBe(403) + done() + + it 'Remove member', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + loginUser user1, (user1) -> + request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + Clan.findById clan1.id, (err, clan1) -> + expect(err).toBeNull() + expect(clan1.get('members').length).toEqual(1) + expect(clan1.get('members')[0]).toEqual(user1.get('_id')) + User.findById user2.id, (err, user2) -> + expect(err).toBeNull() + expect(user2.get('clans').length).toEqual(0) + done() + + it 'Remove non-member 200', (done) -> + loginNewUser (user2) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + Clan.findById clan1.id, (err, clan1) -> expect(err).toBeNull() - expect(res.statusCode).toBe(200) + expect(clan1.get('members').length).toEqual(1) + expect(clan1.get('members')[0]).toEqual(user1.get('_id')) + done() + + it 'Remove invalid memberID 404', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + request.put {uri: "#{clanURL}/#{clan1.id}/remove/123" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(404) + done() + + it 'Remove member, not in clan 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + loginNewUser (user3) -> request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() - it 'Remove member from owned clan 403', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan1) -> - request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user1.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(403) - done() + it 'Remove member, not the owner 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + loginNewUser (user2) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + loginNewUser (user3) -> + request.put {uri: "#{clanURL}/#{clan1.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user2.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() - it 'Delete clan', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan) -> - request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(204) - User.findById user1.id, (err, user1) -> - expect(err).toBeNull() - expect(user1.get('clans').length).toEqual(0) - done() - - it 'Delete clan anonymous 401', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan) -> - logoutUser -> - request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(401) - done() - - it 'Delete clan not owner 403', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan) -> - loginNewUser (user2) -> - request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> + it 'Remove member from owned clan 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + request.put {uri: "#{clanURL}/#{clan1.id}/remove/#{user1.id}" }, (err, res, body) -> expect(err).toBeNull() expect(res.statusCode).toBe(403) done() - it 'Delete clan no longer exists 404', (done) -> - loginNewUser (user1) -> - createClan user1, 'public', null, (clan) -> - request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(204) + it 'Delete clan', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan) -> request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> expect(err).toBeNull() - expect(res.statusCode).toBe(404) + expect(res.statusCode).toBe(204) + User.findById user1.id, (err, user1) -> + expect(err).toBeNull() + expect(user1.get('clans').length).toEqual(0) + done() + + it 'Delete clan anonymous 401', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan) -> + logoutUser -> + request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(401) + done() + + it 'Delete clan not owner 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan) -> + loginNewUser (user2) -> + request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Delete clan no longer exists 404', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan) -> + request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(204) + request.del {uri: "#{clanURL}/#{clan.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(404) + done() + + it 'Delete clan invalid ID 404', (done) -> + loginNewUser (user1) -> + request.del {uri: "#{clanURL}/1234" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(404) + done() + + describe 'Private', -> + # Using stripe.free = true to convert users to premium + + it 'Create clan', (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'test description', (clan) -> done() - it 'Delete clan invalid ID 404', (done) -> - loginNewUser (user1) -> - request.del {uri: "#{clanURL}/1234" }, (err, res, body) -> - expect(err).toBeNull() - expect(res.statusCode).toBe(404) - done() + it 'Create clan when not premium 403', (done) -> + loginNewUser (user1) -> + requestBody = + type: 'private' + name: createClanName 'myclan' + request.post {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Join clan', (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'test description', (clan) -> + loginNewUser (user2) -> + user2.set 'stripe.free', true + user2.save (err) -> + request.put {uri: "#{clanURL}/#{clan.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + done() + + it 'Join clan when not premium 403', (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'test description', (clan) -> + loginNewUser (user2) -> + user2.save (err) -> + request.put {uri: "#{clanURL}/#{clan.id}/join" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(403) + done() + + it 'Get public clans after creating a private clan', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', null, (clan1) -> + user1.set 'stripe.free', true + user1.save (err) -> + createClan user1, 'private', 'my private clan', (clan2) -> + request.get {uri: "#{clanURL}/-/public" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + clans = JSON.parse(body) + expect(clans.length).toBeGreaterThan(1) + for clan in clans + expect(clan._id).not.toEqual(clan2.id) + done() + + it "Getting nother user's clans excludes their private ones", (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'my private clan', (clan1) -> + createClan user1, 'public', 'my public clan', (clan2) -> + loginNewUser (user2) -> + request.get {uri: "#{userURL}/#{user1.id}/clans" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + clans = JSON.parse(body) + expect(clans.length).toEqual(1) + for clan in clans + expect(clan._id).toEqual(clan2.id) + expect(clan.type).toEqual('public') + done() + + it "Getting own clans includes private ones", (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'my private clan', (clan1) -> + createClan user1, 'public', 'my public clan', (clan2) -> + request.get {uri: "#{userURL}/#{user1.id}/clans" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + clans = JSON.parse(body) + expect(clans.length).toEqual(2) + for clan in clans + if clan.type is 'public' + expect(clan._id).toEqual(clan2.id) + else + expect(clan._id).toEqual(clan1.id) + expect(clan.type).toEqual('private') + done() + + it "Can get another user's private clan", (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'my private clan', (clan1) -> + loginNewUser (user2) -> + request.get {uri: "#{clanURL}/#{clan1.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + clan = JSON.parse(body) + expect(clan._id).toEqual(clan1.id) + expect(clan.name).toEqual(clan1.get('name')) + expect(clan.type).toEqual('private') + expect(clan1.get('ownerID').equals clan.ownerID).toEqual(true) + expect(clan.description).toEqual(clan1.get('description')) + done() + + it "Can get another user's private clan as anonymous", (done) -> + loginNewUser (user1) -> + user1.set 'stripe.free', true + user1.save (err) -> + expect(err).toBeNull() + createClan user1, 'private', 'my private clan', (clan1) -> + logoutUser -> + request.get {uri: "#{clanURL}/#{clan1.id}" }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toBe(200) + clan = JSON.parse(body) + expect(clan._id).toEqual(clan1.id) + expect(clan.name).toEqual(clan1.get('name')) + expect(clan.type).toEqual('private') + expect(clan1.get('ownerID').equals clan.ownerID).toEqual(true) + expect(clan.description).toEqual(clan1.get('description')) + done() From 2b29e755fe8f263301955282f074c624c7fdd2a6 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 15 Apr 2015 11:09:43 -0700 Subject: [PATCH 3/8] Make clan name and description editable --- app/schemas/models/clan.schema.coffee | 2 + app/styles/clans/clan-details.sass | 13 ++++++ app/templates/clans/clan-details.jade | 32 +++++++++++++- app/views/clans/ClanDetailsView.coffee | 14 ++++++ server/clans/clan_handler.coffee | 4 +- test/server/functional/clan.spec.coffee | 58 +++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 3 deletions(-) diff --git a/app/schemas/models/clan.schema.coffee b/app/schemas/models/clan.schema.coffee index 47c1e481a..4aa2354b8 100644 --- a/app/schemas/models/clan.schema.coffee +++ b/app/schemas/models/clan.schema.coffee @@ -1,5 +1,7 @@ c = require './../schemas' +# TODO: Require name to be non-empty + ClanSchema = c.object {title: 'Clan', required: ['name', 'type']} c.extendNamedProperties ClanSchema # name first diff --git a/app/styles/clans/clan-details.sass b/app/styles/clans/clan-details.sass index 42e29f123..2f092b68a 100644 --- a/app/styles/clans/clan-details.sass +++ b/app/styles/clans/clan-details.sass @@ -10,6 +10,19 @@ width: 240px background: rgba(0, 0, 0, 0.0) + #editDescriptionModal .modal-dialog + background-color: white + + #editNameModal .modal-dialog + background-color: white + max-width: 400px + + .edit-description-input + width: 100% + + .edit-name-input + width: 100% + $spriteSheetSize: 30px td.hero-icon-cell diff --git a/app/templates/clans/clan-details.jade b/app/templates/clans/clan-details.jade index 6484dd947..f642e3bcf 100644 --- a/app/templates/clans/clan-details.jade +++ b/app/templates/clans/clan-details.jade @@ -2,14 +2,44 @@ extends /templates/base block content + .modal#editNameModal + .modal-dialog + .modal-header + button.close(data-dismiss='modal') + span × + h3.modal-title Edit Clan Name + .modal-body + input.edit-name-input(type='text' value="#{clan.get('name')}") + .modal-footer + button.btn(data-dismiss='modal') Close + button.btn.edit-name-save-btn Save changes + + .modal#editDescriptionModal + .modal-dialog + .modal-header + button.close(data-dismiss='modal') + span × + h3.modal-title Edit Clan Description + .modal-body + textarea.edit-description-input(rows=2)= clan.get('description') + .modal-footer + button.btn(data-dismiss='modal') Close + button.btn.edit-description-save-btn Save changes + if clan h1 #{clan.get('name')} if clan.get('type') === 'private' - small (private) + small (private) + if clan.get('ownerID') === me.id + span.spl + button.btn.btn-xs.edit-name-btn(data-toggle='modal', data-target='#editNameModal') edit name + if clan.get('description') .clan-description each line in clan.get('description').split('\n') p= line + if clan.get('ownerID') === me.id + button.btn.btn-xs.edit-description-btn(data-toggle='modal', data-target='#editDescriptionModal') edit description h5 Summary table.table.table-condensed.stats-table diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee index 57b80b5ad..894214b42 100644 --- a/app/views/clans/ClanDetailsView.coffee +++ b/app/views/clans/ClanDetailsView.coffee @@ -19,6 +19,8 @@ module.exports = class ClanDetailsView extends RootView events: 'click .delete-clan-btn': 'onDeleteClan' + 'click .edit-description-save-btn': 'onEditDescriptionSave' + 'click .edit-name-save-btn': 'onEditNameSave' 'click .join-clan-btn': 'onJoinClan' 'click .leave-clan-btn': 'onLeaveClan' 'click .remove-member-btn': 'onRemoveMember' @@ -134,6 +136,18 @@ module.exports = class ClanDetailsView extends RootView window.location.reload() @supermodel.addRequestResource( 'delete_clan', options).load() + onEditDescriptionSave: (e) -> + description = $('.edit-description-input').val() + @clan.set 'description', description + @clan.patch() + $('#editDescriptionModal').modal('hide') + + onEditNameSave: (e) -> + if name = $('.edit-name-input').val() + @clan.set 'name', name + @clan.patch() + $('#editNameModal').modal('hide') + onJoinClan: (e) -> return @openModalView(new AuthModal()) if me.isAnonymous() return unless @clan.loaded diff --git a/server/clans/clan_handler.coffee b/server/clans/clan_handler.coffee index 45cef76ff..e52717a53 100644 --- a/server/clans/clan_handler.coffee +++ b/server/clans/clan_handler.coffee @@ -100,7 +100,7 @@ ClanHandler = class ClanHandler extends Handler Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) unless clan - memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString() + memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID EarnedAchievement.find {user: {$in: memberIDs}}, (err, documents) => return @sendDatabaseError(res, err) if err? cleandocs = (EarnedAchievementHandler.formatEntity(req, doc) for doc in documents) @@ -122,7 +122,7 @@ ClanHandler = class ClanHandler extends Handler Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err return @sendDatabaseError(res, err) unless clan - memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString() + memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID LevelSession.find {creator: {$in: memberIDs}}, (err, documents) => return @sendDatabaseError(res, err) if err? cleandocs = (LevelSessionHandler.formatEntity(req, doc) for doc in documents) diff --git a/test/server/functional/clan.spec.coffee b/test/server/functional/clan.spec.coffee index 43ac49d42..9586a01d2 100644 --- a/test/server/functional/clan.spec.coffee +++ b/test/server/functional/clan.spec.coffee @@ -77,6 +77,64 @@ describe 'Clans', -> expect(res.statusCode).toBe(422) done() + it 'Edit clan name', (done) -> + newName = 'new clan name' + loginNewUser (user1) -> + createClan user1, 'public', 'test description', (clan) -> + requestBody = clan.toObject() + requestBody.name = newName + request.put {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(body.name).toEqual(newName) + Clan.findById clan.id, (err, clan) -> + expect(err).toBeNull() + expect(clan.get('name')).toEqual(newName) + done() + + it 'Edit clan name, not owner 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', 'test description', (clan) -> + oldName = clan.get('name') + loginNewUser (user2) -> + requestBody = clan.toObject() + requestBody.name = 'new clan name' + request.put {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + Clan.findById clan.id, (err, clan) -> + expect(err).toBeNull() + expect(clan.get('name')).toEqual(oldName) + done() + + it 'Edit clan description', (done) -> + newDescription = 'new description' + loginNewUser (user1) -> + createClan user1, 'public', 'test description', (clan) -> + requestBody = clan.toObject() + requestBody.description = newDescription + request.put {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(body.description).toEqual(newDescription) + Clan.findById clan.id, (err, clan) -> + expect(err).toBeNull() + expect(clan.get('description')).toEqual(newDescription) + done() + + it 'Edit clan description, not owner 403', (done) -> + loginNewUser (user1) -> + createClan user1, 'public', 'test description', (clan) -> + oldDescription = clan.get('description') + loginNewUser (user2) -> + requestBody = clan.toObject() + requestBody.description = 'new description' + request.put {uri: clanURL, json: requestBody }, (err, res, body) -> + expect(err).toBeNull() + expect(res.statusCode).toEqual(403) + Clan.findById clan.id, (err, clan) -> + expect(err).toBeNull() + expect(clan.get('description')).toEqual(oldDescription) + done() + it 'Get public clans', (done) -> loginNewUser (user1) -> createClan user1, 'public', null, (clan1) -> From 2f8831ea72e129f57224f0cf7381b8ac52669b3a Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 15 Apr 2015 11:12:35 -0700 Subject: [PATCH 4/8] Show clan type in My Clans list --- app/templates/clans/clans.jade | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/templates/clans/clans.jade b/app/templates/clans/clans.jade index 89fbbc9e0..3ba399c56 100644 --- a/app/templates/clans/clans.jade +++ b/app/templates/clans/clans.jade @@ -59,6 +59,7 @@ block content th Clan Name th Heroes th Chieftain + th Type th tbody if myClans.length @@ -75,6 +76,7 @@ block content a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')] else a(href="/user/#{clan.get('ownerID')}") Anoner + td= clan.get('type') td if clan.get('ownerID') !== me.id button.btn.btn-xs.btn-warning.leave-clan-btn(data-id="#{clan.id}") Leave Clan From 45c070209b734650ee04c9ccaebc422edb835249 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 16 Apr 2015 15:26:14 -0700 Subject: [PATCH 5/8] Update private clans dashboard --- app/styles/clans/clan-details.sass | 32 +++++++- app/templates/clans/clan-details.jade | 106 +++++++++++++++++-------- app/views/clans/ClanDetailsView.coffee | 37 +++++++-- server/clans/clan_handler.coffee | 13 +-- 4 files changed, 142 insertions(+), 46 deletions(-) diff --git a/app/styles/clans/clan-details.sass b/app/styles/clans/clan-details.sass index 2f092b68a..fc50e7559 100644 --- a/app/styles/clans/clan-details.sass +++ b/app/styles/clans/clan-details.sass @@ -25,9 +25,39 @@ $spriteSheetSize: 30px - td.hero-icon-cell + + .remove-hero-cell + width: 100px + + .hero-icon-cell width: 30px + .level-cell + width: 50px + text-align: center + + .name-cell + width: 100px + + .level-progression-cell + background-color: lightblue + border: 1px solid gray + // cursor: pointer + padding: 4px + + .level-popup-container + display: none + position: absolute + padding: 10px + border: 1px solid black + z-index: 3 + background-color: blanchedalmond + font-size: 10pt + + .level-progression-cell-name + background-color: lightblue + border: 1px solid gray + .player-hero-icon background: transparent url(/images/pages/play/play-spritesheet.png) background-size: cover diff --git a/app/templates/clans/clan-details.jade b/app/templates/clans/clan-details.jade index f642e3bcf..2775edcc5 100644 --- a/app/templates/clans/clan-details.jade +++ b/app/templates/clans/clan-details.jade @@ -54,7 +54,7 @@ block content tr td Average Level td= stats.averageLevel - if stats.totalAchievements + if stats.totalAchievements && clan.get('type') === 'public' tr td Total Achievements td= stats.totalAchievements @@ -67,40 +67,78 @@ block content else button.btn.btn-lg.btn-success.join-clan-btn Join Clan - div - span.spl.spr.join-link-prompt Invite: - input.join-clan-link(type="text", readonly, value="#{joinClanLink}") - .small *Invite players to this Clan by sending them this link. + if clan.get('ownerID') === me.id || clan.get('type') === 'public' + div + span.spl.spr.join-link-prompt Invite: + input.join-clan-link(type="text", readonly, value="#{joinClanLink}") + .small *Invite players to this Clan by sending them this link. if members h3 Heroes (#{members.length}) - table.table.table-striped.table-condensed - thead - tr - th - th - td Name - th Level - th Achievements - th Latest Achievement - th - tbody - each member in members + if clan.get('type') === 'private' + table.table.table-condensed + thead tr - td.hero-icon-cell - span.spr.player-hero-icon(data-memberid="#{member.id}") - td.code-language-cell - if memberLanguageMap && memberLanguageMap[member.id] - span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id]) - td - a(href="/user/#{member.id}")= member.get('name') || 'Anoner' - td= member.level() - td - if memberAchievementsMap && memberAchievementsMap[member.id] - | #{memberAchievementsMap[member.id].length} - td - if memberAchievementsMap && memberAchievementsMap[member.id] && memberAchievementsMap[member.id].length - span= memberAchievementsMap[member.id][0].get('achievementName') - td - if isOwner && member.id !== clan.get('ownerID') - button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}") Remove Hero + if isOwner + th + th + th + th Level + th Name + th(colspan="#{memberMaxLevelCount + 1}") Last Level Completed + tbody + each member in members + tr + if isOwner + td.remove-hero-cell + if member.id !== clan.get('ownerID') + button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}") Remove Hero + td.hero-icon-cell + span.spr.player-hero-icon(data-memberid="#{member.id}") + td.code-language-cell + if memberLanguageMap && memberLanguageMap[member.id] + span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id]) + td.level-cell= member.level() + td.name-cell + a(href="/user/#{member.id}")= member.get('name') || 'Anoner' + if memberLevelProgression && memberLevelProgression[member.id] + each levelInfo in memberLevelProgression[member.id] + td.level-progression-cell + .level-popup-container + div Level: #{levelInfo.level} + div Playtime: #{levelInfo.playtime} + div Last played: #{levelInfo.changed} + td(colspan="#{memberMaxLevelCount - memberLevelProgression[member.id].length + 1}")= memberLevelProgression[member.id][memberLevelProgression[member.id].length - 1].level + else + td(colspan="#{memberMaxLevelCount + 1}") + else + table.table.table-striped.table-condensed + thead + tr + th + th + td Name + th Level + th Achievements + th Latest Achievement + th + tbody + each member in members + tr + td.hero-icon-cell + span.spr.player-hero-icon(data-memberid="#{member.id}") + td.code-language-cell + if memberLanguageMap && memberLanguageMap[member.id] + span.code-language-cell(style="background-image: url(/images/common/code_languages/#{memberLanguageMap[member.id]}_small.png)", title=memberLanguageMap[member.id]) + td + a(href="/user/#{member.id}")= member.get('name') || 'Anoner' + td= member.level() + td + if memberAchievementsMap && memberAchievementsMap[member.id] + | #{memberAchievementsMap[member.id].length} + td + if memberAchievementsMap && memberAchievementsMap[member.id] && memberAchievementsMap[member.id].length + span= memberAchievementsMap[member.id][0].get('achievementName') + td + if isOwner && member.id !== clan.get('ownerID') + button.btn.btn-xs.btn-warning.remove-member-btn(data-id="#{member.id}") Remove Hero diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee index 894214b42..4a039c431 100644 --- a/app/views/clans/ClanDetailsView.coffee +++ b/app/views/clans/ClanDetailsView.coffee @@ -24,6 +24,8 @@ module.exports = class ClanDetailsView extends RootView 'click .join-clan-btn': 'onJoinClan' 'click .leave-clan-btn': 'onLeaveClan' 'click .remove-member-btn': 'onRemoveMember' + 'mouseenter .level-progression-cell': 'onMouseEnterPoint' + 'mouseleave .level-progression-cell': 'onMouseLeavePoint' constructor: (options, @clanID) -> super options @@ -61,6 +63,8 @@ module.exports = class ClanDetailsView extends RootView context.owner = @owner context.memberAchievementsMap = @memberAchievementsMap context.memberLanguageMap = @memberLanguageMap + context.memberLevelProgression = @memberLevelProgression + context.memberMaxLevelCount = @memberMaxLevelCount context.members = @members?.models context.isOwner = @clan.get('ownerID') is me.id context.isMember = @clanID in (me.get('clans') ? []) @@ -75,6 +79,7 @@ module.exports = class ClanDetailsView extends RootView me.fetch cache: false @members.fetch cache: false @memberAchievements.fetch cache: false + @memberSessions.fetch cache: false updateHeroIcons: -> return unless @members?.models? @@ -106,17 +111,27 @@ module.exports = class ClanDetailsView extends RootView @render?() onMemberSessionsSync: -> - @memberSessionMap = {} + @memberLevelProgression = {} + memberSessions = {} for levelSession in @memberSessions.models user = levelSession.get('creator') - @memberSessionMap[user] ?= [] - @memberSessionMap[user].push levelSession + if not levelSession.isMultiplayer() and levelSession.get('state')?.complete is true + memberSessions[user] ?= [] + memberSessions[user].push levelSession + @memberLevelProgression[user] ?= [] + levelInfo = + level: levelSession.get('levelName') + changed: new Date(levelSession.get('changed')).toLocaleString() + playtime: levelSession.get('playtime') + @memberLevelProgression[user].push levelInfo + @memberMaxLevelCount = 0 @memberLanguageMap = {} - for user of @memberSessionMap + for user of memberSessions languageCounts = {} - for levelSession in @memberSessionMap[user] + for levelSession in memberSessions[user] language = levelSession.get('codeLanguage') or levelSession.get('submittedCodeLanguage') languageCounts[language] = (languageCounts[language] or 0) + 1 if language + @memberMaxLevelCount = memberSessions[user].length if @memberMaxLevelCount < memberSessions[user].length mostUsedCount = 0 for language, count of languageCounts if count > mostUsedCount @@ -124,6 +139,18 @@ module.exports = class ClanDetailsView extends RootView @memberLanguageMap[user] = language @render?() + onMouseEnterPoint: (e) -> + container = $(e.target).find('.level-popup-container').show() + margin = 20 + offset = $(e.target).offset() + scrollTop = $(e.target).offsetParent().scrollTop() + height = container.outerHeight() + container.css('left', offset.left + e.offsetX) + container.css('top', offset.top + scrollTop - height - margin) + + onMouseLeavePoint: (e) -> + $(e.target).find('.level-popup-container').hide() + onDeleteClan: (e) -> return @openModalView(new AuthModal()) if me.isAnonymous() options = diff --git a/server/clans/clan_handler.coffee b/server/clans/clan_handler.coffee index e52717a53..80d521a55 100644 --- a/server/clans/clan_handler.coffee +++ b/server/clans/clan_handler.coffee @@ -68,7 +68,7 @@ ClanHandler = class ClanHandler extends Handler return @sendNotFoundError(res, err) Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan return @sendDatabaseError(res, err) unless clanType = clan.get('type') return @sendForbiddenError(res) unless clanType is 'public' or req.user.isPremium() Clan.update {_id: clanID}, {$addToSet: {members: req.user._id}}, (err) => @@ -86,7 +86,7 @@ ClanHandler = class ClanHandler extends Handler return @sendNotFoundError(res, err) Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan return @sendForbiddenError(res) if clan.get('ownerID')?.equals req.user._id Clan.update {_id: clanID}, {$pull: {members: req.user._id}}, (err) => return @sendDatabaseError(res, err) if err @@ -99,7 +99,7 @@ ClanHandler = class ClanHandler extends Handler # TODO: add tests Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID EarnedAchievement.find {user: {$in: memberIDs}}, (err, documents) => return @sendDatabaseError(res, err) if err? @@ -110,7 +110,7 @@ ClanHandler = class ClanHandler extends Handler # TODO: add tests Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan memberIDs = clan.get('members') ? [] User.find {_id: {$in: memberIDs}}, (err, users) => return @sendDatabaseError(res, err) if err @@ -119,9 +119,10 @@ ClanHandler = class ClanHandler extends Handler getMemberSessions: (req, res, clanID) -> # TODO: add tests + # TODO: restrict information returned based on clan type Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan memberIDs = _.map clan.get('members') ? [], (memberID) -> memberID.toHexString?() or memberID LevelSession.find {creator: {$in: memberIDs}}, (err, documents) => return @sendDatabaseError(res, err) if err? @@ -147,7 +148,7 @@ ClanHandler = class ClanHandler extends Handler return @sendNotFoundError(res, err) Clan.findById clanID, (err, clan) => return @sendDatabaseError(res, err) if err - return @sendDatabaseError(res, err) unless clan + return @sendNotFoundError(res) unless clan return @sendForbiddenError res unless @hasAccessToDocument(req, clan) return @sendForbiddenError(res) if clan.get('ownerID').equals memberID Clan.update {_id: clanID}, {$pull: {members: memberID}}, (err) => From f80a73ae9bace18976052a9303aab794d446ab03 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 20 Apr 2015 14:04:22 -0700 Subject: [PATCH 6/8] Decouple clan type from dashboard details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding dashboardType field, private clans automatically set this to ‘premium’. --- app/schemas/models/clan.schema.coffee | 3 ++- app/templates/clans/clan-details.jade | 2 +- server/clans/clan_handler.coffee | 1 + test/server/functional/clan.spec.coffee | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/schemas/models/clan.schema.coffee b/app/schemas/models/clan.schema.coffee index 4aa2354b8..acdc17cd9 100644 --- a/app/schemas/models/clan.schema.coffee +++ b/app/schemas/models/clan.schema.coffee @@ -9,7 +9,8 @@ _.extend ClanSchema.properties, description: {type: 'string'} members: c.array {title: 'Members'}, c.objectId() ownerID: c.objectId() - type: {type: 'string', 'enum': ['public', 'private']} + type: {type: 'string', 'enum': ['public', 'private'], description: 'Controls clan general visibility.'} + dashboardType: {type: 'string', 'enum': ['basic', 'premium']} c.extendBasicProperties ClanSchema, 'Clan' diff --git a/app/templates/clans/clan-details.jade b/app/templates/clans/clan-details.jade index 2775edcc5..545a16e31 100644 --- a/app/templates/clans/clan-details.jade +++ b/app/templates/clans/clan-details.jade @@ -75,7 +75,7 @@ block content if members h3 Heroes (#{members.length}) - if clan.get('type') === 'private' + if clan.get('dashboardType') === 'premium' table.table.table-condensed thead tr diff --git a/server/clans/clan_handler.coffee b/server/clans/clan_handler.coffee index 80d521a55..df5a6ca0c 100644 --- a/server/clans/clan_handler.coffee +++ b/server/clans/clan_handler.coffee @@ -35,6 +35,7 @@ ClanHandler = class ClanHandler extends Handler instance = super(req) instance.set 'ownerID', req.user._id instance.set 'members', [req.user._id] + instance.set 'dashboardType', 'premium' if req.body?.type is 'private' instance delete: (req, res, clanID) -> diff --git a/test/server/functional/clan.spec.coffee b/test/server/functional/clan.spec.coffee index 9586a01d2..b5bdf79df 100644 --- a/test/server/functional/clan.spec.coffee +++ b/test/server/functional/clan.spec.coffee @@ -23,12 +23,14 @@ describe 'Clans', -> expect(body.type).toEqual(type) expect(body.name).toEqual(name) expect(body.description).toEqual(description) if description? + expect(body.dashboardType).toEqual('premium') if type is 'private' expect(body.members?.length).toEqual(1) expect(body.members?[0]).toEqual(user.id) Clan.findById body._id, (err, clan) -> expect(clan.get('type')).toEqual(type) expect(clan.get('name')).toEqual(name) expect(clan.get('description')).toEqual(description) if description? + expect(clan.get('dashboardType')).toEqual('premium') if type is 'private' expect(clan.get('members')?.length).toEqual(1) expect(clan.get('members')?[0]).toEqual(user._id) User.findById user.id, (err, user) -> From 38cdb3d0578170e2203f0fe7bdd1f0c359816375 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 20 Apr 2015 14:16:44 -0700 Subject: [PATCH 7/8] Update clan analytics --- app/views/clans/ClanDetailsView.coffee | 5 ++++- app/views/clans/ClansView.coffee | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee index 4a039c431..c6a910946 100644 --- a/app/views/clans/ClanDetailsView.coffee +++ b/app/views/clans/ClanDetailsView.coffee @@ -178,7 +178,10 @@ module.exports = class ClanDetailsView extends RootView onJoinClan: (e) -> return @openModalView(new AuthModal()) if me.isAnonymous() return unless @clan.loaded - return @openModalView new SubscribeModal() if @clan.get('type') is 'private' and not me.isPremium() + if @clan.get('type') is 'private' and not me.isPremium() + @openModalView new SubscribeModal() + window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'join clan' + return options = url: "/db/clan/#{@clanID}/join" method: 'PUT' diff --git a/app/views/clans/ClansView.coffee b/app/views/clans/ClansView.coffee index fc7132b33..c1b6b96ca 100644 --- a/app/views/clans/ClansView.coffee +++ b/app/views/clans/ClansView.coffee @@ -91,9 +91,12 @@ module.exports = class MainAdminView extends RootView ) onClickCreateClan: (e) -> - return @openModalView(new AuthModal()) if me.isAnonymous() + return @openModalView new AuthModal() if me.isAnonymous() clanType = if $('.private-clan-checkbox').prop('checked') then 'private' else 'public' - return @openModalView new SubscribeModal() if clanType is 'private' and not me.isPremium() + if clanType is 'private' and not me.isPremium() + @openModalView new SubscribeModal() + window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'create clan' + return if name = $('.create-clan-name').val() clan = new Clan() clan.set 'type', clanType From 9fffb80b3f5daa8fcec66c9de543152e2318fca5 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 20 Apr 2015 14:30:31 -0700 Subject: [PATCH 8/8] Update clans private info blurb --- .../images/pages/clans/dashboard_preview.png | Bin 0 -> 65523 bytes app/views/clans/ClansView.coffee | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/pages/clans/dashboard_preview.png diff --git a/app/assets/images/pages/clans/dashboard_preview.png b/app/assets/images/pages/clans/dashboard_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..bfe7f2bce9477453d0d9c703650e878f11ee029c GIT binary patch literal 65523 zcmb5VW0a&zwg6gPHoCfyWoFs7)n(hZ*=2Uw?y|dV+qP}netl-<&YU~ z9BfU@t&BlH#6uI4pya-pqYrjoWa5Z}5vLN84V!}u+YfQ1Vw(5@de{-+CB~>Ly0GL> zzKFz%AVVyI3}GRIL83!|9YCmmOK@h;6WqsQ4{j}8ZD~=xZ}GW5@%sFDTzpsN0U=+v z%ArR{`a+-`LjikhZ`#*4z;M+G23`yXN)J|JB@;YFLqY=bX6@Mom3{nb9dBOVdHGsN zI4V811p*S3{i|zyD?iWEfs;c}9tZ-BRBmZ%3R~0+ZtRX?fxQ1d`-^*&tcXKDsdC~@ zIM6-&4HE=3XdImsj2}@n{T#xoG-d-}~Sz?LxhY zsQ7mXX~cUZJZ%?n4zG!F581=<(U=DTCEFjjJa};};yA$TUnDn4ZH68LZET1?HiUq5 zJ+2#?5q)56)3cHa7{cFQf>JNOO6zO<%;#em7gLxVf=w1=h+BQ49$sz{3t36`*1VsRg3i zgr zDaW7$tIQ9T4S9rd#O(B?1@c}HDO-V*i3#9LD8e1Km&hz6eJSNP@yC`F zmYCHc5XNB7fh~GwcrZhM`@SawA_knX?_*d}FomFs(dOM+J!=E-1_KP6)k!R3 zr56NQ)mXI~gdeKTSe8*Z+J0sJO2$#G;J?UT6KN*I5}Yk`pJK0ztkYOAUa8`i(b3H$ z%_7pJ_EG8(^VSlO>DD0pPAHb6uwB*zf}3+Xt$xS+qVi(@!t;_0VF9;>PC~oXmTfMu z=^q_N8CDS%8YU7p9_EVHNzRbamY|T>Kvhr4BR@cnO%XzAN_DB&QN~?JRJl{!BTu2g zQE?-Oroy6tpr~EoAy=>IEGVr#E+$>vAaJMzim12=wcE zD;6&+QiIeJR;}vE%dU>tx6HR1w_@lH=-%kq)GIU_8d&NM)KoN=YVzf6h0C6 z4Kj5bre2%DCa7k$!-ca4>E#*aLk+_&b25Xp8xGy9y(heiAwfmYcgMXpB{Yl+L&i`!+4;tzP;sV+eCm)*x z+5!QxyKD=$TUPK+NG13UD+~?+rX1c2&jck2e*sMfP8ZLc{Y@zFN8iFvu~^91ouLWx z<6=^I1bO^Co;=AsD1$KrmBF>2%2E4+4gK4@xjQF2K=dCp8})8(q2qBqq>o7_bmQtC z)~Po&b;hR2l60nYd??Y8(NTOjpNY6*75cIyRbPL99pJ$Gwf;+0OZ7tL!Yo(OzoPzbsgWQ>thO?X_BL*+aTZ4d198F6j zr-R5;q;g74jT}eqxXNL1#6GsOm=-2m(wX{Vd|uxzAEc3FOz%)!VJ zoMXLNv^m{TchCLsy$!ZDtoApPE|ZVf$CgFoJk$ce<<|wLig_K)Di^nm$(EP<{QHtq zpH;n%9H-vb$Z70VxH;GbH2C+eTOq?7rJUj_pZMLv6MP1~8=tdft9tjzgG-UM+|&Mp ze&c>!Np{jjpPHwVx1tgn2~CfN6`S=&ghuJA^9`5e^ZSCD0`~$TE>PFHGov%=m1pn8 z8|}rBveDnJ@h;)6NC{90jpx(Lwbql1usZ$g-ycT~DCd8VE8{;pJU*N|t}iN;t7vPz z8}s>mzJu{XX;|D@j3DkHn&5isU@NcQGu<8rbMCk7wAhA#hd`e6p3t!@u`uJP@U+#V z*nDG)!c%wHTacg3vz7)*L!}*$_YZb4TvsP|_I)~BHho@CppKzRlR3+(WaMU0TKH)= zXbxz)mxEbXTelaz%eXdhHDtZa-z^H6GEXXHDzmrV;~Z;sH<#SHAChqf;u>Pa(ZM^@ zUV1t)9PDIB<|KR2zUp>(^uLbu(>7Mzs%BJVHXqoX-#H)LFJL#YX|!0k$iAE0u9o1f zW+tV(u&vrGZ(3Yx&;6*FEV%5ko?KPEsJd8d!ggwW)Y)r)eV2TJx^TRV<`(~Pb)dh{ zJ1lmJXNZr%6YH+i+Ba+FIQVgfya zQUTIX6Sv1lOguc|pc1|o76mj`RJ@e?(nbif`BGE})GR9On9ni62c5dscfrd-j3QcL zx?P*-2s!QY>Ei>z>HW02Yz@u-6S2bBiK{z;fWV>t_5A{p_6ze*KnrfJq~@e1EyZbQ zYfYt=2DXEX>1w;Si5M{8pzeF8UYD;q~nHy)yYjNtt9{8uv_5y3x(I9c)# zsY%Nb2-!Lq6R^-S)6x_1!V(Y=a61^8a4HCk{tNuiKOQ18Cnq~jIyzTZS6WvlT3ZKG zItC674mx^9Iz~pCKO<-y-EExo-DqqaiT{b@fA9z!I~qEe+c}xr+7SGOSKq+a*@=gU z=r2P5{`_;E#%|{Sp=9IuFShr-Txbylex+N2iRXd{{;I7U;iY>{TDJ$ zIdeB-D|KOWYhxS7KU3pnWZ~fc2burV^Ph?S4N~ntkblJRcgVl>{0;K2nQ+QFm>d5Q z(qE$BW#Fd!KW+aS&rSE2oc<=ae=6l4t$(z_3(HOSZw>Rp>b~0bfPnCWNC*okxqUg) zhSJ07pZ};ReKe6w+TY+}oyVjH7e7K|A<01%{{qVH1f7%Il_iRRY++UyxX}%{y43VY zxf)l#Av1H|`q{pm-a7w$;Y2p7mkQ8iJ8x&R?O4z7;(9e%m{Q>313~)+3i~CdzI{O$^UAH{o?1_42Q@(SSo#cQ~3olKm{8)NPD#9>YX3Zu9Hvu zi3CwDGwO0K_LD@d9i?RX`u3o{iKZ#Ptjh<#YLDU0z zKRc$GRK~?$(w;*6>541;oZw@eOOK@ne#n`!n)JS-Qtu3&5_|k+N)ty%mAjG$JZfVN z=gPhRHRr$4x#Ixu8K7Nm(rLt(Mx<*?n2@R`2#U~Z0N2#uv5WhXV!%YqB*FykGn5G{ zl>F_=qs{Dy==tGW7b82tHPJpPvLKX2aPmPwh`zqmc0Kb?A*g8)1m}C8D)6 z6ry|!W!3}6la72A*F*lSZsVft8l2ZFhxhE41&~tSwGc&}>k+AR^9B<)F5qS^OY3J{ zS-KVdF=Q6k8!s}9zKMyiLO8nfg@4mAtlx$(0k^bdZH{*h2@iMrFnR$kbZvB)V1I(% zBgjO^?$5d?-MoO9Npk@6MukID)Tla!JYA@Bb*dI(gLVDx#6BKD?@uP6NiO?3c>H9z zZ8Gb&e>A5&fLUdoxpi~{1{c>xtGX++`3soLC_`@GiDLT~tt%0#B&Ak4W&&K($cDH$ z?=?xtn+0_ICfxQ8PBbNje;eRGl$ni0fb3#OV7$y*SN$x>R5XxnkA0(E;GoYJS55w= z9|SQLz65#|H$9Yiko`%GU7|JMLh}h2ZtoKG8Jqm5!mG%uOEuA24kF+OP=u`3PDRv& zIw3p!%p4W;8`*Z*EoG4@BkX-!1XG|d8H4Ox45&nB+YYq-bu~+UZ)ZZFa1ots;wAZ= z@v0NmYiX--tFO9bsjUq{b;32b6Tyr`6*{Xs@Y;~k?$`ne&+=iXG%^FzQxC`mX6Y{o zTO9azNq-6A*A*bJI)kK9WIbOJG!9r85G@)6N!$8nY~_?D73 zK|H8(KGb@g8T z4>!Iy@ZXA_t6j9ASmoV2wWvg6&Y0nzcVKZrd+KwWXF^F?NFt0rt*o#Z#bx=PFB|G! zwp_okMSmBd9pC$?4m#I<5gTYs_h4`r#6vUyOZSSEq#>_juS$6p^Qe(dPfYdS4NkpU zBBJXkaoJg#fWNE7>a${7U84N&rAGtKY?zF&u7_NCUgD&g0hg|LRPJ2_CrwL45HKan zR0VvZw~fS2_C6E{V)6Z6go6J$64*81@@7P+p0)?WPt|8P*hj6!O^oe#fcUoi4qU5_ zER$$3V_Lnw358%cj{%e}G#D&=xFr1`o zUp2KqIhBCvI)RFPOQC1qgy&OEGr!gbTt^WaKK5(SRhT_wmvy4qI9TiDssHz4?+EfW zIXgLHZx9nJ#Il=JwKlV>jkWB0aw4jVsUG#pBbi*9Q9)?3GyzgNjFD4S>Fs+|&~G>Q z&Q?20l6}SPZW(OlJg(Xu99l2Q$AK}uV#b_7neG{wRRBZ^ zK@O--`-c1|o(2hzuVxToguVhBelZ;}ik8I>$o*VOn&eyJFVK%UoiF-^BAr!lOD z+$mM*+9B)qv~7_I5B@PxpdFlp7HLdagD7i!qM znLq(%nH8nZUyJo^H2)97A%7UI*$@biA|_k*Qe=uGuK@%`pCgh1qV9CDyoiAKYXTyB zadK8U>+o+qOZi@%^w#277H2XJf%kdsBhh_f zIGEwMi%B4e;b^W{~UMou?tm#uYIrv=~fM}~^WvXyNjjhHhN^ab(eXY;9F z5~rM~0I?ZH^0zAbZKg}gpX))te3T6ln$e_i4SHM3X|))O5Ot@B7^;6q(n!Ph^!qQB z8wCbL%zvZ;Es`UY%+J3{4>ht55n~37=>(ve;^_De`ecTz4yZE(f%8_i#W!F%{)Di% znz@-kfQ0lz(3P156{74?}iBVk1uN}2-&w9O9F zJ!})A=NG8a8w=9vq?q*kd$2{lGyzIa*3NJVO{|;WLC(vzj2>Y%>Qpu_LgGE$BXfz! z>Av@AGsT93(lymi|880Hcf#?qg9H}JLtNj40J*J>6q42)gFb1oJGgsk;H@Z;dW0zc z4==Ew`fS0_kzRX;DqDh@2!OW-=oQ2T4j`eMc5!16#G(BAA2z8Mu zaM;638R9mOUkVF8cj7yZj&A&$eF7Niut|&qZl^~$H0hK~0jiLY-g_=4vVn>2_#?GW2LzT8_cTeA)9M?!- zkj%!j{Wn6eC)kFLY@uOVB6>q=mH%Pl@XvLfc|WH{z!_z$O)F|aovRI`Pg07wJJN<6 z0WlYU)+fzSjoz>6fX@XFJ02P$CX<*=dTE+dS1Dx8k2sk%B&KFXCFKT#87;Pt??29F zJeaA~F8eeXyP7KvM-_%#3P0qbEO2ec>J>Sf#rI|->Ql>%8$z8m0_Wsm{wpH#!uI>K z8Hy54?s>2Dp%)6Y^_ZeY^C8GD%FGrz^D@Utf?|7Fzj=2~1SoFgows`eJv|Z2pA0&e z&vjPxyMyyYXFd!k>vqp00qllutR|H; zUc+uI$f-Wg#klM5Ej55KxXG!bUVOeh>v8rgEBa@Y%^&p|R6`o? z&eRcf%x49{uH^Vq-MXK}in-tVH!WeVmbGC1QtZ<7H+DTWtg$bzn{%@$kajk-;>2R7yr>Jdic`xey z7*k7_hh2q5%ZEKopO#&hY|nPXLr!n|rtSs|fLDat^nlItUeTHAVDuHVm~PHAUW09G zSAFZm@E3vA(AZM{=sk@6Rr)Ai4Z?+m=`m+9MJ^kk0zh;fm`{exD>(iy1UhR8po{-h znri3YV)4J?125<=`e!vAY2A`p_XXu$OBHt>$3h53ziML53)JN|vb)4;8)+H8iDVWj zP|%mBn=KFeX%lGG=|vzYY<#8WziUg0=u{g(Ewxgvb;5Z5QcG7J^ZK|+3+^qkA=Hduo`wCxH6y)hbrOLK_=DI2`%f-X}SqqB8Jtg|lpx>)h$@IEib8WTX z?r&b0T8H_E$W#^Zws{|G-=RU8Qgp7II!-_s&`isCqDs2Rd%?~?qw+GDJ4ix5BPs} zNG#Bt)b);hcI@gimu)9ExkW)y7C`g!-e9^YQ-{&41*Ekb0c*hDl zvUF}OoXGv(nuPSL!Uij!fdw~{rTcHW{V`j#FDvS!UAh1`Nc}U&H{O)8RPk{@nYAN< zWqhbRpT@?Z?BjBe8kw=-{k?Zjr2Hs@8y?{Y=Qvjage9Z;edWpG?Ag zxgt|$Boc#(2WoiIydB;v?Uem!)JS6hOJt{WY*1jcLuIBOS3SuLheEY7ix-!ovdB5s z;qKYU_~e*)mYVG=l?1WjvWt;UlH-^&4w12W!L(1_aCic0YBX($r8y(_w9^r#^r%QY zpgf|4cyzZur&2OdR+jF5F8O%2hF1kAoK?l0nnP;?H@ZV7D=Gn?KIo3Po}n5iJAkG7 z=poT!R-HgZMwmuj7_Gd4 zsvajsr3nS%78#aYGoK2#My9aAQRA7XN*cfDdh&V2pf|SGyP$} zlNC?zvM$M%n6H1Z-URGB#? z@adu!I`S_|qz)E`_bWonPmS>NhR~SfWM}Mk@N=(@OAQP2uKKNIWo6AfteV9)i)gJS zJOHC3Lv7TDYS8)YqPjvaA~(V_Vk4nrr_Y83@!R?<;<&lPkw7L5z^!P~prw#*GIUm^ z&PfT~Y>}~789Tal*^Qb*QyUkaSNk6eV5*6_#cAOZ{r{^XWS=Nud0}^PAAw(8vPP}3 z&*CVt=OPQEp7}UlI$23~vGybgha5w}4C_I}-Wo^K*T)8noRlR_g_^+_! zOcuJYM(;0DjeG{gVR9$s>2A;ydPL^U+D`h!#AZXnuqw~_DB>@F3Z4R3=B>jg#>A@3 z>-$VU5% zCjvijT;6J5I6o0qRHRyKJ?{~PB=gEl;gdgmWMXK3i@B|px=p}X3{Cfe3ey3c4}M|7;J)|eGztA%IS)nj zdQcY9)9|n@sW74#6;kexgeb=K4_z;J2VOJ#A4D66EFDA`cS4*{#Hf5}EIWEGY1{EG zedP}$4_d=<4NaAwWaf*|G4`@6SI zgW;jVn)~g$nyNZoU5qoe(5e3yOLXsudr z5?xclx)=F>+hZjma(`sbqS2#-0G=sRrn@NxZIdWp{b;S*m$}O&e{Jb=@yW}OZetO% zij6Q6T}sLkNj^qa=7yf!|P?Y_x&aPMB zTP_U-7p2h#nijmGvP}r@G~Q)DZsgsfTZHKw=WNB4rQtwDsbF6dLtU_bH|7N_;9|AQ z)YboxQ&_;~{Q+j7iAzpRsD0!9gM-e+WX5{Y|6AuE$oH_%{n=+cG zw_3KV2MZ=qTzV|Sa5z0g9SIpRuuL~cQg5Gd?uj+>(T6;fRh2o`M&i=!Zvi8)a;o(~ zQOcYnqhTm^%hlx$WDX-C1e-epNz$5A7Yx~EU2+L~DbveJLG_wUM??0|(8NpAP3TCQ z4geMOU%pfoAEYzCR_zaEC%fK&_&Oc?278+A#TzXU;~9AQCg^%{?hFx`h-bwWLEUMv z$?Y(_mWVHCM%ntRES&G=nB1NOWg9X|m&HNTCIVuXRPZIsn2c^9@H$)@K0Yuq?OL_s z*y5iVPCQFBu=KgPe4rCpo;TMxo((i$dBEs_rw@0t(#oC&?MV8fLw&P;5H%@CH*=uJ z>Vh@D2C%_KJDSBgXo?oj+X^{E{7&IN=LG}+aJLTNHz$DcS+LynSJ8L~RLFy_Z@+Gn z*f#5RJA%YM-tn2;n6_Zl?5EGLV>=!oq&`itPk39gE5_mIPsgCHw|qV!ZqwN8H%H-O zt%0?Ue}r-$olML^gG;JTVU#b-jL9_0WCx7kIZ}IS80(g_#gE*?;raF+jS~sV=oORfT@@iJh`bIGE2*a;FosqwiAkBzkn;d zeX)F)dos#)HPuVktr-JZln0`^#)>^FmKe4?05X0+T4o@%{&)_{I}l$O=SNw{;NB2x z%Dnz$HKF)4s;fJ21U8_*{zl6FEQ_>#23C8$l)53{-$A&tdWAQy#&^O+=8Gst>b~T4U zw##)JTct8C=DY%q!{!X#;Q30J={4zR+UbopnB;EvsXmx(u>_xO1GZ5n)t^4e=hr*R zC?}r=h7BmYKRw)e;QELHb^@d`mEIo0O6k8*T@R3H%Ktj=F`V>?+MFQoS*_%W6j{~2 z^7zI6JfnFv3J_v=>_pm5xcv+!_U1(`$zUN*vDdMKIQRZ+s<+j}{Lb}{>|$yVC5AQq z2-4JFpqW9O#6SBA&J>DcAy|ucnZTm5&vJ4T=`khVzL%zPt2oz!`}u5e2;hhr1yk1d zGD3N#){493_UPkBfGnSOut=6yK=j8eNLUmvcH`O&Swt^J(`&uEnc*eu z#cA|5599sx_RWl?H-g#uq9hX0G5cfV;to=0C#dPz&|p~oYPI96!rI%ACHrxN?vATZ z6K21PEYXH<_`F8^~JK8*+LL?eBZjkFxd@=5P9@SsLxNY8(S;-1Uh{{@V!1@F1QUr z@Z3Jo%B-*a8&(FMdSwTQ={lke!&wbd^)W9zZk*o^TR=VErWM%Nz13($rMRBIwmn@8 zx*hwYG<^oqRhLdg<;GEa?8-@so;faQF@h~>`=EMrEQgQcdjpykqXzMPyf9}(CwsCg z-(Wgkk2u=qS>f5%oZx6vIFLU*UI5fOl*Z`>CihhS|lbDLN3U?bC0+bBgkW2XYj z|2|RCLqe8gYL)*}!mq2CFBIsN1&cZmWk)bKTdu?We4gZd8p~rKL9H@M-i^o$zv`j7 z%r(2!!tt2UuHtmfbsn;#{@g>eDpc!*tMNw(&!e5A?@kIvLi?Yd;Y6dR&1t`0-VR+Qtm z#Sc$S7t|Q6!(xg#r7;UDX+hXf{)8jDNFD4mC6Br7I%UA_kXE;1U{>>qhHdULW3fL8 zO~f8fTY`S#k{Y__}vv+6_z8Nq|2+wl&s<9)CDG^gKphw1}k`JHlRW~@rB zd`Nm1u%2B>fvbZuMA{=o3xd-$v`b((q2~J{4R>`%OtnRq61Z(bHzOImn?hldhVTxO zfnPo#u{h>m>WDTI64BRq+5QuaB<~ZV)31M4sw;do>&HO1%#vMLuA|;sf=s2cb z4G}*&TW#J8Y6YK9%oXVxlYJ{U#@#wRbl1Co)3pmEPBDEn2c}^ZKA+dC)tUjKD6s>K zsZ@GX_`b5da5bi}n_UwkVxyC(_JM1H6g0J;pW1WrQaCVG=9#&7a|wtri#g#kOG>g< z@hIpx9~G^_x9Pg<9lR{>D}VezDaw48{N?iwwsa%SXSK2rOm*P_a_kAbvh;E>UYZP; zJnUZ~y^OE4rolmkQYljlAN+i^mEyfKb-NB=(=XHZ4a#Z{F3Xe67Yp)KqV>RPv{j3A zm_+>b^!iF~uf~z4Az5`3N_7<;;pcWZ*(n-(wT#ep=&ax0=H7JA)(PCa@Ss*d7^Jf0 zjb@^4|JvEPL#RYUQPi(q&;E(t!`&bJL5wJJorX<+wz!xb^QM2xqjGkbLv*_fjn z3dIFFd-XIqT4f!N>s)SP4Ve&4hMQM*Q_{4vz-BjC&bE7%{NV?GXrVOLhWDuJi36$h zZD&mKvKQYr8^V%K=FiOns7&u&h6qZSt>qEpBQaW(w=exsX^}=-kX^P+Yv){YVuv`3 zdkNpR1301$KA7fHI<3g?JkA*t&{z#N!bk0=#|zl6?{4WYN3iW)2V|=a`&q|YPH2r% z80BL4K(gM?7oWVmA#6B4h?IEUF`7rIg;#?k*>84Q8BF$!-cA~8?H-!>`hSRx=b!F~ zq{w=coUztrW&Qa5<6lM9t9N2Ovl1PCO??l&!sXrwe}>IY$XD_3nTb=YBGmyJT>3OA zBf6P3jAwzu!@c-XUqHR`yjcU6-uRG^yE)+)GaULAyPWPONWKXn9~t(Y3l3fpZvA}g zlmL_Q@|{OhJ^{9k#N70pDqv~#>~-!MW&1iW3jep{x; zCJ8b1Q`DW$MNzW9LwXcsQbq)wt#SLh*qJ?Q%4?-amY!ozPks09e3ukg!#>emk;w8M zs++Qb&)2nVjW^oQsY`jS`CM)J>L_MPbzW;3B7kE!#qdBJSydaCqpDeYt`s9ki|DN; zr=$QBa9x2Ry7w(1o16?+XT>Ye?_u^nZAM;;zw&c9C=fX?AWGe^G=2i#Iwvc7 zGV9E9&m=`oDCPD!J}rZae$fSJms{O|IB(PJM59LLJT>KL23q ztJ)?s-1x+SahiN*Y4F6l)iBQzt2(GavTfeZewuT-iQ8OK&bp z^D7cXf{b(HW^2tE5g#%sM{AdS*?bDclXxpgO|cLD52;LY3JC!Z!5PK2R(b3AzFPV#eS1ULXgb?Pv&f~ZEc=Y0~kF&H$U+k`?lrz{$(LktI*)KcQUUs>>8}o z?J1iYv+oYBhN@{A?d9Dabh21Np%z%I>?9!vtt9}$nus0fZWMJ~eRhCE?GH`;6(hyz zC_AGc!y{)@ckR=YW`q6=`^ckm;Ze=ZYAiauvf$c%XC8|WJ#kIHG$RNw4mFuMVhfGb z6sq3!<`n9`__V&-rZBwXjbRo8CbTJBxC%s^Re6Xx>bU9Fc13RbSqcn>G&5Rb@6ZpV?FjBR_w(Fe>oH~`>Kt>D}h56HAl)wSK#Q*F2~g^h2b&nT@1{3#8{|` zdFPymNG9hw4!?=1%%)|(voo?Tbi&5BCalyi##A{I_QSsk(oJnNp&B&IU zkjV9|yO^O4_Y!lGlwI@!+TGuBBVIpyU-3j(qzYoIWon2Tws;{|=Vk0x7iN-I6RQEBQ;vP{2nKw%g04NFU@Z{tBB}vXon6+#i zKOUH{1Uhq$0~aGb@6hOke&^|#8mX4KA=W#OQ+J^|4eYl4eAvGP`{4_seFw<|Z?mEt zKKM(I{#LEa+fC=s>mPN`Jajq=0)$na1YJxCGDWC5oVD&;%|k!3&x*KDpYevW%vsZp z4RWw)k{tmdYUWvkB6lje$q86qb-Fq-2d2;Hno7-aVhtj%pgppSg~NhbaD^jkG(#;Q z01fXIu_a+$*Lbf)#{24QGJWrkVbti@eI~FVGi$TEyJ{E@aubrK&N9uqMGKzy2fGHu>rCLLgK~`AG*ct#|=c4rBckgVGMS4=# zBH4$f7qjbZqygb*nhZAhT<(VHC@M=#PL-XdnfER$LRN1yOrF4_=cSo(VI9>t&N$h; zqS>+Y!rkwSXxG99gzg+k&RCgv_7|t8#u_t0Zb#g^mQ*Xxg++;PadiCL$L4j(hGTfA z97-C03~gx<Va#!Ob3FOr-aIDL`L%s(|gkre0OZH)HnfuGlcWE%x2Z{ zWg;({+lu~R>d^jny8v4gqc^Aml=ZifUS65UG8;K2FM30Sls-Hxc9Izh;4oE;$p)-g zr}-=CZefg{c^-^T+V-FKNCuAy=r;G3*-O=%Vc*GcNuCS=Xlv-$C>MxaCySLL$9n-R z+Ws{G1|l-EA-wPckY-|*JQ|I*XgAAMs9yV2+LZWvrg^JI4LRjDP@LvSo8E>+U8fEf zoG1vjM_{Ms4a-wahQxqxc;685^QNIpAJy8nU>XXhi?VHIVPCkNIUS%7fgUp<99w3}?eT5Jw!6z#3GFDZkD=RE|?v5{4H;tzmGV|NKOm zJj3Cqn=f*%|D6K$De&l++p&Q)&;AyeAk7=dD=L_kfQTY5I7k+S#uHJuc<$d`i1kh; zb?#rljzLN{O2+f7WwqR_{EKgGbI6d&-jp@eOK4x?Iy;untqG_*vG#qrTMchD?Oqb* zQOoENUwrU8S3?l zv_~I1&&NujcfT8Y@Qoiawx2m-tmtXt073^O??eS;UnF}7BF9Ej_Mf4DYp&*`sdas_pmDkXd_0;g$)dTG9>Vz7SOa|%QP7|4+sNGcO=Jk1m8lokj zMT(7}9|lbEVWA~f!@n4MwcT(VK1cXd4;XxP&w-boWL)t=!b}vQzl1kp5J$q-svQv)|C#|HRajGbFA1dC<2zVA)UO&pM zml#loetl|T;yEoVS~L>=Og#MFb4RUv$F{b36j!g{JvBuS|H#J6b1HktzyQ+AtJpQK6+3@@JUD(*^;9!GxPt z+adGfEkjb0quH3=0ls7m)ZO5W?)>CC)|Y3T@uGU(z{qtu>7^-Sz6}=3h3tkxNE5i% zHS=0R;7!Tt*_m)rFCiobMa&>P9;;gyN?kWB9!Q6RdQODh3#%&G5g8BV59jJ!7ASNl%p|%(++7DDxolv{WO{F*Q zc@gLNs*g5U+zKW_BNpFpTa$mLOpMp!J%{enzEW&~D4uOn4=lNL>;8R59#7KTL9uXh-?JGK}*8I3x5=hIkvxgxxoE7JU{kjxGBF|lwF)HHs?Uhu5-@BD30uL^zIb5|}(DQbf|EBE(RFNOVm(iHmhxvP@E7cy$fVL_A#SDjz6}+@&+K>G;*H zoSa)X>9Mf_(J0+e$b?C_PSnVd7Vv;M(8 za;j;Q$Q2gdnM~!ZWV!K@&TFc?hzYqqXPTHs&?ApCL|==doyO|A4=;r88(cI3sOfk5<1YK+AaBx@RLyV-Q<>rl6N2#)9Gs=xt3>)-@+H_va6U|7UK_%-DDxRgU8 zJrah*W85%8wj#^PDP}CmFxH}4tg9Rck%sgb`~n^RL|YK zuH9NzQKs?^1Pdm1om-y8%bXwJ<7&o*MZs=QA8XiEFd}o&f_nJ;1PjkNi%a`(`Fs?c zb2DM%%JbyZ)cn~cD9xf8S3;7QC*#M=0hWT^`iO;i%t;YiH7`de4pUZ^Q18aZCfVZ~ zQRV3_T~o~>36J36biA+7mF;gh+DE`8HaanjQ^u4L#DMxYJENHkPTpjUOixrCmeJ{! z6GgXC(>Q6HUdc(`P*hnSuvtV-HLbOktr=8=9&QJ{<6_MaQ$C%btY24-@S`lbTu^zEOdeCTNNL=9&s7G`9%U{_UG8F2Z5#*&IBr*QSu`fvMp-WB z#eLzDiuA8{0y&I@l`j=v9bEzT7pcTT``o^@5ksbktfm}4S?|t=ndfWye(|1ZRe#Ci zys`gtrz+)h1h!4dx{K7**r11>vBkMBO^WC6F&*8|+ioj**}?Um0bmRP^K*V$Z9lHQ zHN|(b#VSbYhmz@{Cc%H&en!D9h`BYP6FqJSb4I^K7Z-)F!(sv&+;IO!Rqgq;Y-$^QQb zyg)<0;5>eR$p&s%iX4ZfA*BsS)&if z$O4-%V)=d>5=(Tv^G6_fFIc_kLp=4`cNjgY6h5a5O^wZ%{__iP)Wku;FqZcLt!*}b zSx1r=5(uvSZe1ry!jwB#+cT+#{7z2=rY9co@)~_Mwi%HFlYao=xGQ8QNjP zDlm}zVU6=X#l*B-*yYQ?4H$?5j~^!vD90g(Ob{*%9x|lLX(l%(BNZ0f4|4s6Ex7r* zdyt!%O4~kp=psyPM3NWa(k0*Fxo7{4OxjK+z50Lfw>QBx_7^mRx&=AoPQcJ3PNErR z+9TLw#pmy@LHV{LkX!#LT3c2i>!5S-)|~&MfLt2}4;dobmKS&kfkYEx@L_Y%hn1ht z!QBtuh~^O&;FyzmmfDH=@7{y;S!d$T^N+&2f4?7dUi<{3uRSFy=!rJzCM;KPX~xQ8 zKRuBLFnn+U^2jHoa&t2>a(OHOV91bslw{L@O?$C!-Bge5ZGPm?Z19*tnb@_X1*s)@C`b!nM`bgt**PfA zwP4rQ1|T~F{W1brvw^ltqS=9gWjQD>N`uSkLo4}XS8Z>=hh~JFBC^B?AX)s>C zr30ok8zzk`fW^c!#B>ryI|~y9q%Y9L>R+t#MEjH=e}ZI*w%VuYVhwWNTPG3(KiUZR zgX=CqO0W;&!AtTa7)R@Of~?;&nG0KXxp3?+F2l?TC2+Ub;Gw(k#rt!&;>7GS3EHf2mjw7GF%;4zh*6(JH5nPj-eGLVp2lrxwC<`hbzLE5rbgx)0?-3X3@US zUW1jJTQF?YFr?dfPwVi+$@dJlYEB{0s|dsz!r~gcJfN{71J43FJDn&h%!NJ0hOJG& z$G!|aF?I$(S``R5Kh2@_||z}Vpf#2&tE^qOc#U$2k7c)Jhs z!fPV8hu2?ukzS8`(XV|zGD?PE$Id3$MjeX)xidVoa|ou@k&6Vq%~{tBtniOPif1VD z(poXTpBX#WypHyP$K(BX-oW`6UoIA|N!i_N5kbLE!lKB4iU~*H&WE4HbMLR<{e7s^ zjIl?}#sS3x;B2Zvlgoqt!z%>!p4`OJZDu_1^cEOtKy+8x(N1{Ye_j#({p?CiJZ2(h z9hQg9>o()#T{$>=nhmdivK2nN9fLhi%=M+=@#_vGzhx`l|8^UiXdlmXt-i;b95Z^Y|=o&p;gvuWf)v1WNYo+E?2Xk!Dmk2d1H7b?+0ui095c4Enl z3jF2-8f*!`J;6f&+E;(}wByX5KuC}*0ZEGFM~Q$sf%t}p^b~qqM=o-0_7vC~8ep)d zW7Mb;G;f@byYBcWMjUxOY8Lzx15f@Pes%mfyz;Mm@l|yuhG(~6VO>A`{q75CpYB3> zy9w0OjOw*lK7mW_x&tG{N#(`%wJWf8=_VNJKF9G>hQhi2Bi#GQCzv>GAXcud!mqBo z6+anQ8af3nSeK7HVE46asw) zfmpZN_{J`E&l&6@KV@cTq+`7=1tsZOI5p3WPI6J`AjjRoH2arMK1n0WvN3`Tp$WBb zFpkWQLID0$l z4>_6M%K-D19Ea;anjd|Of-}Ei5&OTSx$L#NM3wCR|1 zyn#*@ZHk_iO>G%h2KQ(*qm(wb_q*!x@|+j&@<$7C!C_~|l#ytKn`US)IISFaTsayR zM-#5RX(g6yFd?fn4gTQcOMnbw3q6_hrt~u=7T~_CCey5f2anvp5TAUz3n%6TaPR?z zxcSRkp{$p^P> zyAN6Y(~v{B{`*TeUcYAsJ>Uc|^~taC%7Qw~%BR%v*5jzs5o%ki-( z8xN98MaP=$xbBTg9Cg?L49}s{?T4J+jrGVU4@h_Qk}hV<0%8c+(<$vZqV?*8S`Q`k zwQ|rVy;fc~px+KnP$-=$Sr=#~y zPoYmorxv9NlEsk_tTH0i--u7&n?oOTDL@ms&V9I|8OL5slRj;!n0eM8aM08Wtg&vx z8fQBym(9asdp>TuXEw@=E%@TS57F*$!IF}T?>_i1Uf%4)#rNNh=_5+S3@T5Kj6C!w zIPvE@QF8G47@4yRSG@ZzF2DOeqRGeVxv$_KPri#oAH0OV0>vuP2VH0#%!YX%;?>L= zfZlF?@$#LRdCMm>+2VmMdo-@R@BkFrnz6a=T{PG3#OAL*M8#Rx1GVrj1o( z@D9S+7tO{&V}{_dPv5|y6R$;CO3z>4lZOZn7*8$$Jc!dPvl9oWz}K#Q0nAEn0URtI zpO`WA(89mTfng zM}{uDZ2*&I6ym$ZUbr%~1&3z(@N^0{O}=8z79;9vn()#yc6jJ`9Wf#Uo_3nmXJhw= z4BgFIbyuH|?lGP#V-zPOQW7N~Bh-aZLNa#~CrjUrI8ohe#z!KJu}P4K5v7xrG=q*; zpG*?FXym=6N?0u&_|X~+6Lw7{N-ODP+*L}{oSu}^It`=JhYN{15e9t&VdfsBeCRM0 zf9X1lB?9PiE z9C7lQ6gyE*Z_M8NXbs9r3hB*eaO~rUeX*8--W1Z>7yfdozoQZTM-IcN;rT+>p+m-D zoM{f6UOySTJl@j%mnJhNA9n^WxnMdiK=$I)V`t#+_x%SOC*O{O(j2_=%1bcQj+^x~ zJ7uDA+e52rrqYZthun&(IJtu6hBkkR)$6vQuFi!cM$-afs?&b8F`a2uPzdyO1Y(UV zQO@$NBZgS-Nf)S%zG>b;hSh0BUX+qC+tT49Ump)r$P(MxWW)~oOCdvYRdX6PHD#i} zzYF!`Ps&E6fv$5C@w;iz;Mv`HfsI`bJGNJ1XZ1FuI%`qs9*K#+_#N_Ro`uHIXQFld zx!5-1JXDUj2Wm_I^ZFnRKKGQ)g-TK$2K9m zH-3b%OuCY7M^D!d%zfe}%%g#XLmJJritjOp(Cvz;AWGoJLWAO0i|f!yRj z6wnmI<}D3a*Fy5?)1b@dHXtM21e=Xk`I2Z}^hVDaWc-7Cef?y37vsvSr{Pb(Do1&? zi554CJ3#`y?nRQG!QMrIs$^#Zt6S8)|(^`h`n`rmjxdg zx*sf9h3OqFa1ht3#!On=WJbQB6{+;}S?RK2V{H@Os`KOi@xV9|vwi&*ln(3%E6;w? z(PGdPVpeL_|oypa$xQn;^(JiHr~iU6Hmz*2Bfgqx(+ln z(je1D?NPn0HGuq5`Yt=YzHcHQ-?mord!Qg^k42`(KQv=a)}4=@zG2D1A*Y^>+y43x zwjD7VUwya`r(AZe@FnJvt*5id{80bjS9V}%jswPO^4A(sgehYxkhQ4^e|()j_OZ%> zKfL3>KV}oUg!iZO>v8+j%P_*=!pE&vJbF+G468TdkvD6Rn)WS`dfT5RPR14$ieAzpMo3Osm2#Ze4)0 zXp4@Ax0x~j&Qb){HNj5Cw^&I`yJENvCQLkfC^Bzaf&U{n2;WXSti$>vn~Y)+Skdn* z^g>g~i6H7&{fkweC>Dt*!p%#0p@b}m5+_0vVv9l?r6@!m)-48#$mA1n#>jW@_Vg{8J9b z9e3S;RR@p8k_BsU{>^tHgN9$eUGlCr7W((jUmr$B=VE$&*^2MTE$=6n-Hih(O0niZ z8$SN%Evzuu@##P2AaD4A_{ou{W9}_?;U5jhVasP9;^?dHLSg1sw6?nG<4YB|@bWWp z@dHm_{9`wxBF84~i805n-H}rz3W2_XfbJUCuDh6OhbcYulv}g29-A867(y4}+(s*2 zXiLZKWQa9(crf2?#e~CW;>qFVSh-?7eDBOd(djNSn+^1;CQZD?Vc*^E+!&-PydQ5< z!%pPcXvWaurB$Rp%&nylS~NP*xnTjEYZuY1q8F(&v&=J!79Z{ZyLu~5zN49DgK1Sj zofCdC_$;*ltBqdj*lEv0?HlN_g`UOIa5bis;i?PHMS7No(TGD%!9RyiMLi8B#~*(t zN{ab292Anc83Y-EEJlnPPm9Jr91d5f1CD?h{fjb@Nj}gMPZ)>i4r+&+_Ta6XMZ;jK z1!VF7Z7FvEYqSkycTRjU19-+c#l-Aqt_4kKtoSz@} zq<#7PnJ8e%(kCi68MB|Mpu5E~JNIpcDO zF6;UompD$Pxd4~ldJm16a`zwuu1e2DeoiI={Z7FBgQtlxmiKYLqQ!yS{1VI}_q8e8 zchMqj1AbLLQ0#Cp>Da5_rR_O+_H@>j51_4`-?~wtJ$Tt~cgD#V;*G=Xw1_wZ8Ar~- z!@~|lEBX4Ke$GYc$J_2OspyUo6Q|F@hjWjm*>+hSc$#_knH+Kij7*2da;6c**sHdUQ)I^~kL?jhk=Y$t8~#X*}XHqy2)1L!Zz%{p}fK3o+y#XP)EVrp6{{V^%cwgv;YWumXem6xW- zqEK8!`!({6fecJ*OHZLcS)#>!f3ngil`DduRSwruMQREPQ?(Lqr#R?+Ao@tVYLu21 zgvf>bj5J}$a$#=1))2cjq|yLBg8pRSHj}pdC3A1Zq(d@69EW5W;?efxM9|AimOoi~ zeOrm7laQYTX(BBPn!O=2;l}#}q3&c)2CPJBC0!C1wPa!V^yyxMQ*>F^?;Z#i;~c%6 z%FQoCaJnKag$6N$B?U$O%TXjtLqui_8azan@(VrskdNkr%Q91FQFn?M9$7!?J(XF~ zve2LYIB;oEnNBX1h&z`?iqwoO@h6CNKip1QW-cn|uS=lvtbAHXO@CUT0@{fuU(7mC zn(pU?{pl4Q1+C9}<%noat6zn{_lN+U-BHJvZj4=K(=Nn-*Np+gDo{%M16J8v>ABPf z3vIStGWJj$H{%fOq9ALEG6nnuPBT|-9pJtE69nVDTP&0uC_7ofvV zugUF3)Hhl{uAgY78OW)VfMX6Cf>Zcsx`=#GFRtN-4xyu<G;P=PaRL{-l@~Z*su;f{)NWCV4p=^-wz_^H5}Y8Nz2+rrn!vA0@()F z^U$D&Qj`foK^meB7O?;!SL5UR1ewO12?niAv2sL|5HIxiF;KEEq`sL9NTI7@gjM&+GEU{)4%h=@>I~5YieuX_l3DCop0F zIqa_ytaNd)qd(0Io5|4R{c_noAzqNzYYu(TAg{2PjOU%O*X_i@`IY3ukVDH&+hCxX zM;Gn+n$m87y~#sG26S{%nkIDl;|ldO`|KfK+6^^nWMlhilGldff)eD>(>e$Jpdbo* z^rH<>Ay#Tv4jD2I)%7sJdP+R>B4U>7iGpcCE7Q9Am@@SJ(50DGFH4HzG8rP%;TlQt zBZ#}di}wtK1i=j5oD4y(A7R?~xI2cCeA z=exBSR@eYbIcPyxEAr`8`zSJmeY9HCM5|Vh7+!$O3BZd6^dr9~7a72OQSkybUjps~ z+67o%F&M)~jKa3<+b}$D2?lMv9M$fL=;&yLWvUehH?3?X!@1FE!W+wIbt<_l7|H2$ z{yH0NRc59y($Gi0=&NgGWO%n2Y#1?e6iUm=#9`GBLO+t@th&hvbT_tlSDy%qI^(|! z`JT#7(0C0s6H0W^;`_}6b=VhFiQ4hKq1fJ#8CCi23Uw#U-q2}p$c)(~YK*(nU7Ow2 z$CR%s6aor?{T+dXj9o7omG$%$nXL3wj6V1Pyxw5IAT%0@QA+ zL??Y|?Tb~LX-QZKa`Up`+*M1jc1&W%*GGHxdcEDv>}s!fQd3iL@S%s}i!VN+uh&dM zxqmT=4Xe>fhPAOX4_M$p&Hd>#WnUTDFABT`XYzZH$S$B{>a z5mTofERJR5>z@7n0IUA#5YXNKb*15CclBY)IH?$Suk>ZlR1gNr9*RhY zJ!xC#`#vVY&P3BQT3IrEk4u}(xsa$L$;gb;@3?x0DygwV^b$HHLKqUP5()u@z7(bC5eN|8h>}>@2 zh8yc@+uLoEd~v`39@^2GNUWpZmrg%0^a-OL4UfK}d#IC%9r_SCC!+Pc3qib0!T{}2 zEAcc?bqWE6Kp#LL)=&}G*p=g%nKyi;FQl#CSc#h2Mr>=WM(OCWSbX4ta5U85@vpaF z?usqQE6Bm>s&+KyxRF160RHpsM>wKj6beg`dGtC z*QSyc-IY7>T+;0%%@9MEB;`aSk5(3=wd9NUrtnzj`)*o>*EA$_B6m62sRN1hArYD` zdNG4Cj=MmjG75n{jzE`TK$nSY?8-i;(3VpfG^2X!P503DVQ!c^y|B=ZDaO13^raRL zJnNR@i|w=tZZHg^&li%{0k(I3hr4SR;=nP3(XTLGF1+-&^!2VH zVPx}%4^;ROBhcM_t-Ja}po?Yy#VSukCetu~GBTJpnXnh>lNFte+IvKs44&~C+Gjf$ zR5}^`5~Y=N`Yi()t}>iVR%{oY!eUfPA)pZGy9jg{Ds*E) zTw|AKLOB!^R0v2NWe1agl-ipP$WfCFY;s77sBX}1WoEeA&0 zw~+mU$4{Js6HYn}Ma9KpmRK3O;_#@Sy@~*xh%xWVT-V**dao+?g9-n>dfitLW<4WA z@9Qp6kJegb>-%CpLT}lfeNk1VlR`itu+Jk9*Vwhvj4p?#U;CifI+Co>DGlC%6@&2G zE3U@!rAx7F*z#eIXFl z*pO?clBrh{F8~uV`h2ciHP1zT%QT3}3NDhG{PE!(0)Uh6{ zVwJ~GXSa5DD~Q1;i<707e2K>0$`2ESQ;7;smQHJ7@3)hHUI`rMXnIB~OQ5yHNRknr zC~b%?l;~EM#3@;no?4%d)l4EZv3OOvLO>y~zatRS*kwbJXLd)7(A*Px6NsJ|p~B#0 z^O;;EOePb`2GAzIF#^lypbXs@0R32t_xk;i&pZB$)=;eSK6)^;OKcxeejib0A5lX) zD7tfSAJHp0HIkzieHLBT^?M&VJCzy=0foT#hd`_mAgZy;YRTDDX*l+lsw}ssMksOe zw`)T)%#pfD**%rtTYXe=_E7|SaQ{rYC)`G~t1VV}q75Xgd$RP2>Y?ajBM|Ml$EMyl zYZ5(g5~b^vUMARBf@Hm-k`kg2Pzdak2=FivZ77au>~b%p@wl(`NE)*;-q-X~`s||! zM7sidTh&-?#47LY_NBbeec2$f206V-do`V4N28JLHQo36c@V8Pdllu_ z!uNV#V>hj86aor?eFK45!$6e1xb~sJYu~_5Y0*~^2%XBoL@1wlhmvNA#Sp7Jaa9tc z+ndfdYWiymKjS|y$n4K z^XO%9K2}JCE*gz&o8?BPxhzs2E{hkUwGoeA)?FQ(da2FvWeZ8KC(}Gs#8?Dzt z^2I8X{nOLw<>}>1TG_sgC9gg&`8d|v4xY#2?B^f)Bb`N-a`FjK<~2V4<)G1WEUpQy zMAR@J^T=2fGF`A-FldG1S5Fs&EX+>@QV%B9%N0k18Mu&sv?xq3@>C=E1)VG*YF(a7 zr4FICs6I@?6!gbsl#(Gt%LSrB(&(tT9a&9rh(|{+Wk=#7`2>%U6O^mtc| z=r`E5EDg)iN_P8|^c-tKB4p?|G-0A`P8x_uJEwFs`lOI8g;IrVOHUI_hx-$xrzoE8 zd>&{D%VSUZIPyhZP;{vIxb#dFlr6^o1mrUdDXrFCZedYTM?fFa;(zFyAehyki1c<_P9u%n~9E>vcV8v z0w_f4ADNdjyNl!T>FYSnLE14gkM`9t$>0rzIn@)sC#^Q@c7b#oP=w)zP z(&^KYw49f8oR+jQmSr4s(9>!q$^Eii4vET%q}S_}T+3SDl4(1d{G{5~UEDfjS1XMM zfGm@-UV&(3`ugtDvG`+^vpiWRV|`g_O%$P7M`t&L4|CyiZ z8BxTDPgG1#P!s`0IpP5a#~qjR-NxG-XLn}y|NE+^W_o73duBGA7rO3dySnQ4h5EX> z>+9+Y<_CdJGt-&VlEtm^ozl#CA~W&aP7f=r&}w>F3);ga>O7fQ@XCFzs=DbeFtxfK zuaE0Pd*LPCOMFHKKYm1K*z3kqXrqkOTGIwTQ>{O3*3%qUyyO8$eY{**#8AY{Q_zuf z9y~tkL0pDc@YKb_-BY4L>mUh?ldoXCT=67>6F3ELQqjOfX&D&_D~#ABgJl-HteqVz ztAI^5B3|t0A%A(;h%DLqTa`)ijoHIEjqoRLv8iEu;h*?1Ntai6N*TRMhM_<66h48s z6pa6sZHQ-~k->6EFxOv|VEQ9=SFVkeS?nK5)Wr;D*~1=i6>lyS&c~$4h%^!=R5U8Q zGkn}3k||0Tv87FrR|>(1-AAOJiY`hslf6x}8c)L@OnWHWkg?9W2^Nb=9TT3k%?N)6 zUapljf2lheK3;#8fl8@(Ml$}?e}uY>u&#-$jL5p{F(qO#r9UyEJ7*WCP+n0(t}A88 z6_snN>OqX>s93ZNy3DLgPGukiWe@5twM#FFX=|g1K%Jj>9Hoy%f~K*^8h27M-gfzR z=~fCQViV7;Xsptl=*)R`@kE|xoa7~*YrI{0s`;tl%rfla$@pkGeH6SouIa~mOqW+8 zKJh#qJ;p<;w^VLyq?nub!Jo+S`MIk=u@-Un^m5&znxYqgZ(Y3%yYk@EuvYeb8 zJpTCO*nGGZC1qu5J!bJ=?1DINJq?GDTRsKetel7gXooh^dm9fUZ%R$n5wt^#BPI@F zU-MDqS5GBO6fxb63523cXefaGW6db2nMR+)-hmP};vfw7_ae~MfdcXmhq%34p%MR#%Xb&rJ^qvL`V=fAI|L0wzrC_6d3#Ebp< zxqW(mRh=Vmks${{K)2#kE!5$S?>kw^XIENI3a9teav_XZr{* zqYSO;g(;`FG`UVJYk=3xKiG{cFMFkWDbt%JIq7z)=sDcs;%;MA}HEA$9o+P$GR zc!S*8H^b=DSv>zyXg%N53|PG zJ=xih#QQ*;zvAh$kwvF#ME0V#!$*A8VXpUFI%6)KW9c2WcV1;Bkuc>42M16#lS}$Z z3u6lxUA!3T>tmxGMA_^Gj=HFOm=4@PM;p4m2T?X>p>h`N%Bt|}N$qWJz(8jk%E>=C zG(?rGZ6mhyv3wne_tU8xMj6YS9M3(x$uE-*QDN&Z&-`Ml2Qa|+hnb_*i<>uDc&5DiC4Ck-sU8ZrB+qp=>(319qOb8vtniNfiG1z znIo-8czva;669q@5vnu$KjMJX3+4Y*@Z`^)X);gxBA_LzYtOG5soF>GCHV>>A=NSn zR$hE$4R|-W7H}e5nW_>gfH8`J2~Fbp#^lDeC@5sCI95k zXcnF6Ue&`Wy?N5p7#o%l(?w#^v*ydrK_7en!gNSe#b@Q_Au~50xhe9`^yewN3x6l) zm>Q|i+hWUt;u7RgjvHJHxE63N;99`7fLI{R7uX?riI12g=H04BR~j{DGFMLRdU>Lz z)Lfzo_7Uyq3V#X0I~u1t(zqw4tAvC5l6Z_(?fX*3s&4+Yt4=RVx?H3 zx4}QYtY*BDA-rYHYK0F)Y$L-M1Ze~vQ}!V~>>XBk8Hyq0For#7l%|Rt6`mN5n^d-B zud5kvrH|qthrC3ECI_A8%coKP9w5HL|N=esg8k2aGdgtKCc%G7{g2+~w$f>CuaXLgWacL23T$+iOa5VEaB*drL(VEla`9$oj zWJiq`XDr7d$W6d2q6nSf6$d3F!pR0NNQIBEj@l&wpQuk%LzTfl z33%Zf4HTg1_4Svd{M5anY!^Yx{llonMR`nSBqB3jgy^mN zOO=h*N&}>M=wp=BR1eH~=D1)J`9NFEx;ahP)QU%3ni*?WFe)$>iKB}pk1nSv z&m48q+ngsfak5N0n)!{wnKBdknX-(inWtUcMC8Oz=V_hwQIjV-7o1v9MiZEhF=yA5 zC_Nn3#ipwrK7h889#8mo^Oa}YKz_}yX*KHz5h;rmFgmDjlL;O|n*UH~lzw%{# z=(gKve2&9Xjs-1~B&cRe8a7tMf&Kx!u=zP`edz@>HXOzfKc@x;_~}(V*$HX$4AK!GQXVC{A*iX_*}V8ypTI#CUmWFM}JuAMva!2Q2tPxC=eN(o@N?I=8fZgdU|Bg>b88RhvH$ST8&98#Bh%08t1v>7=&mRNAv(3fxQitW z?3mc4r)WYa)+w@>P}n$&GoiY~F&n{9V2G2Bvf$%m#;DUH=xD>wyU>O7AjN^-u|9mI=|T%V3%%Qo*yAPrUwolM!4qy`uqFSw!1vM zH$S%tKmX~S_5BYI4lIth%8d!`S6{?mQPoq7&lv6@4shI;UqU;PC8GUnqg zufGIbeOT3dZEWRW%6$-VIwf7-Ge+Emx45UH&y>i`pq~xsd2AQIf)&s)4WVZ+ zjOFKFkIPooDMvL*XOd@5qtxiVWloQai-HVhedWm^v~F-M;99`7!0Bj#xTl}##L?N6 z$7U~o0gjQasjI}tZhJd&**hW=LOk9KbanM2lQZ7h+B?wD(1MjKmT+CuY}I=k;$)iq z{9Nqba}b~X^#7nZFIz49q&%b)u7G}k@7}osKf3EX;8NtsJF*es*1Z^-c_jwI0hF(~ z8cWuk4-c2(85qgJ#s{~e`oXJ^v-duZGhT+$6&K?-zq}i7yY>C3uBl0A6jD$k2_(N{ z8djWHz;b3`^MgOZ!~3V>vTNxDjx%T{^2pW$_`o*~<3B#W9P1aBqKD&@Kk(_7P?oy{ z`MpDUVox{Dtl|$}5Uc<`3bKUvI|qdDZyHwNvn$yI#Og!#TM9rV9N1rFJYoXD(LMW}qj? zCF+b7icY>ZRVuVoNS5zJtMY__{7iRPl*TSS3UH1%u}-8E>|}?eG6;xZl0wI-t0m(g z4(;8Bl9Ki08o+(u{|tWLG7q2s@H%+r@CJ_Pylotu}7-p)?$Rgr=2jt+2bJ4~snKo*_taG)1Q8(R?a<)XHx z3YjB=XzLn)XC#R3?miS&R3bCjkJk2X6qHr+8Q{UOjvf?Ol(V-wgI{C1(9Rd)GJZwl zhc4S%I95};bgl(l3%C|I=`3JsBOIMwd1A_#Tj}8)=pA5BZ7vE6@|E)|PVK>iN72#I zN7x8vQtRk%Kn~BBs{a9481BYJrpl{k5 zJov!t)l^2(=SIe#Nv`|!(S1-SiVx1cJQpFL$*L8Q0UwvcuibVAjx zN_6I5eC8n5-?$JLEh$&%ocihb>KF1jWCS?U;lmg2-Gktk1NiICA$;VWOYr&&it)?4 zpT*r@B5cvaMynP$4`#HpPd0;O))-t z%?vai0^a!18K|nr!Oa)f;L}fa;^>habPolwx1|>&{TY}uw}PKyS$^ti?q#wo&S{q} z6=M>7-Bj(E%Keopukqomj4(cqsZtjdm&4QBj0b;oHy#>Tjc?t49SZs3XUoRl;OjsB z7iP{V!=Y_2Vd-n$gZEx{K3;h6UVQbg`%qDxgJbLg{`~)YH#(oX8xIH9UHQH(#_{R2F@y#X10-B^3W`|zq| zx%l?y{s&$D3XHVv!N`pEJS5xIiI^+4v5aO$D5@VeV55^sfJBYJ%KHpj5cGW z1*M){*uR4#75J>*IjtXim%SZm^kLs$8-cO{&YH`IkG-AQIUXE%q7yZ%>QR-y8QYp_ z@Em(P7cV;lD_5;fRdiCEjGG-9kxz{w{Oz7!pkT=cETogD8X6hWF~%SlN>K+!&~aph z!xr)tCmAO#&1iM4O!o_6_tQhTc5N*_bL%|3wFVg})Ha_0WUZp1d=zkC6(yQm;@@0Pf_}4GvXAd{vjCme>{}WBv*k6q1y#vVN=o4RGKN@zm;nH6;;@}rnU|M;$8k?OsR&k8QK0aiV?teK5 z81Hxw2iBIxI}yCzbal2YNE(UiDd#~hy79BSzJi=!C(eESdEiiYh7fyKhtQ6B`09sm zMNaEe`05{C#@1~G`0GQD;ccJ)Ha5)m~}^|98`$?#D>WVfGmG zqR&^3E3UkdTjqA7XyL2z>035n`=7pzjr+FYg~1U#v8@a5{rJb>X?Pjm{Or##`!iRe zZ6F`l-trEtsUE`D-gzSyeCoIO@cg&p>)-kj+OjLKa9%y0d2%mSy`~;d?CQd@tCp*Y z!C^j^#`>F&C5Xc%lh0&4Zg|9txpQY?m|L3W zvIj%L`4^mv{=NZhdTtvozVIy7D;yXYKt>_ABj)M}GQ7bl=pI9v9QX9oPhg14iw*ZZ zjlI*ZM()+?aK<&j(hw(S`5waX3@*^`3*&`?Yw>`WTTpTf_1=LXuKC~y3JSZhdvh&z zJ+={ZFT7MuSUYp&s!lwlRJ%`cUK_{EN<>xHqV)`7nTr)ivMTgk)nw`&H?QA^os~6G7mx|5Y zMI^VN5}$sr2S2)ZFK+sy2e({P&k21Y?4p)!5ADN_$N0&$FN}pNJvi@@W$66*c6{jzwkn1uX1)}-WuCgmiQzIlkNagfSob)*p#uF zDn;F+;-itV3Y-AC6m^~eNpUO!1N~TX=^OEZS54)M-|hI#^jf@j-3+xx@XY0lQ10i= z#qE&`iyP2*umyFq)?&>(d5Pq+W!R2GDvnvxi=;6}g z<%0S0>6#}Vb{iD22N4`kHpmD05PN$EI|AIAHHQ!8eC}vegoegOv>xl=@C7e-TpHl9 zn6sq0ih^wEnL;>pU_TBW+=rZjX6)#!$7?=&J6o0I*xwRF4kt?W(WxB@b2+jg#|}5q z%jHsNp&|A%*ZT0r*9TBpIDp5dWw6(k+pYF@;o$y#ILa}vQ>RUzm{0H#FK^$Dz3e%f zhxv7WWvFp~+GlI2{2~qknYFACH~(S}E?&F>6*9K4zX$*OzuQr`bS9S5$qft}z2w8e zFvkOCp>1mezIIO&-t!-;F+0?WmTxy;aHtc#eHnP;JJ;aW!5)0`YcC>HP=|8zzv9YS zIDbhIKkptw7e__!-_wfum(If*W)x%B!`tv%ex^NkuoaK(8NzF?nS~6FY3!lB>rCB7 zx^Yk8lAZ<-v4)LB#_NFCuss=!dfZf3g2D*+O0aU#BEAGp$2;D4Bi{O@Z{h5FzJNSV zBpu|7x!g4)T=#pJ!;SI_ag7F%PTNrNg3Ytq3_^}-~9sW z26y0NAN(+``R;E~kn2}lK8HBGM?xqp8;PBDi_?BWVIlo8@8INaYogDcs{;*TPAnDZzK z7OcUPzfixgKZ6TzxCIq_49de}+|MATtoz5cfNKHQ0;i4zk~q6u#)oB<$!OA6zp&|f zoPF-ubZWEE&!w+|d~hFYZbk+hkZd}+J)w3KayWsPpYdg>GWn!GJkW<8HYA=|9LB(h zb*eqP-96oCYiUL{owefX=?LUh;4eEp_~UjueViP^xhHgRrR#`_(a)@63)a*bL?2gt z@ED!YC>jc*vWCt%Cw6pnw4$TEZGxQL$hca58r|En4=p9DK-XV|@i*+!Gc4uuVA&b- z@cySZVco}{#Fs88#g@k!v8kjIzrC~;kKTI-?cPW)IUCL9o*{U-Wom{$9)L27vlf^b_{QP`(k)o4q?p? zoAHNBOYp-#w&1W!Ww+szpe!)%mNt* zaMD;e=NL$sv+gQ<6k)2cM{^JY=+=YPyd|@ByMMuDo4eQTh@A9x31Fx{bL4KtP;_55c;`x7m9>2Q(A=HLA2lMq$6Va@S8+R?> zTEMlyX=8ynx5RW3b9N&SQq}d4V3sf+>N$R{!qE6Qnj7jdZRQLN^MM=W=fV&ly5}sK zhl**lP+VM$p1xi*Hn-x?k)s&qIMYDWc3ilw1P6LDkj)2uGG|wdma)ODO-C>@lMZrr z5Qo{L`$rDX%|04H={0O>rD>AUYATwQ}L`_I|H`AQ0L%lp@2{oXdNcoD#buU&xm z&8y^6XI`v$#WF1P8a+mSt|k1=!Z4=Q=HWvhTfyG%LFCSzg58&}M<}-dQ+0?p=QD zI?|0HUkz@qFGYEQPw98U_aGhjmY)Jux4h{NQx}wf?9z39vSalAOa_ePr;AU@HeH4> zd@Pn!$hZK8)${P0H@p)W{!)WiMh-51^T#p2rIl%gSh1Wvq>__ehg)yH22*&C&AI+P zs9UxV-F*Rk{<~LV##CcCK-Hpkc;>(_QCh<5m^1yu803235=v&DkDIE&=Vm3|`r(gb z$G#(Upr_#zSI$R8p$DJ-^ew0?VUMx50ylj2qbMzs$8{c8cz83iisVZYhdOaR><_%* zGSn4|e6e&y0m*cOYXR2+t_4mr3&cGIO^r{?*>!kEN{_6}DxO<6%-)bAD48+^rF~sE zpJUdx?%ju0E~E2g>rPH~nxVGxedPHa@N?ycKCZdjv;S`>;FfDcUEJoEy}W~ik#5(N zE`aRhCM!Z@aIyxqX78=HuNAX4-h>andlwre_NqimrY0b>3}?(QM7;$=9MRGRn2-R$ z-QC^YgF6IwcM0z9?(Xhx!6mr6JIvq=fx(^Sy>H*{FX-F1Z`ZA=bLt#1B|dLgFVl9& z@OCqtWDXH)GF6VbaCr+0Q%H+~b(?*+;2C-2?EDm;2rs6#lwIA^xw72`4m6CjO% zthkCyLzEB!WoJ#+Wu!-pYCW0~hiq;U$JV}ID>**}YH6>R65De7*AkiNZy9=;QR}bb z!KNNJ%)V7y8Zj1F#{CP2^S07x5u*k%&!@vsVNAZxOOyG4h^*&zSf&RNBQt~po$#k# zYU=u7h<6ESQKbGObafG5awGmeA6oTm3&dvg)oK+|7S|PcvKb47-`g%D;owIDzc41B zf0vI-ioM@wCS9?NeqlO&{2!2G*aB%hW5=$jEiHH|FN|NmWx1|E@{n$*`XxzG=2OVcB&Wb2|BgszBr>f+zjss`ym>xrEyDs{`Nt z8bN0_SBpd%i9bAf__Wi0HdEJC=A23(gs*B+7_q8T%pumZchkCN|hSf#uSS4<^7&rZLPsHkKYK^dEHgZi%___uxRN`_1P7t1uUv~E(R|C@ z!I=cFcg+kZb$|%dvQ_@s{IB9;-V@%Nqc*q!Z#m){#? z61%_FFr0bV3W$Bpy${BefvRe?1c8c=uX=!P`3?IND); z+hK0yI+%mGHU5%G%|Ih5-8j!@WtRePW%5yt+t-*(Rl0Gn{N^HB{vt>y(t03H%fED710D5Kj zJg;rtN7#*3nUbTckR=2_o-4+a;P-w$Ho$bmY@}iSCF)7t!^X2HoO@A!Y1SajYoB3; z*EPa1P3(U=cKcbQ%Vsw~wv%~+!*r#J`76{x&SQ~GURlHj$ks=BeC#!_b_^vA!jwYNcO}*WhTKc%!6xQ*) zLS|t0Uc1pgp!Z)8)xlVYxEF3nVpUi{gpT2d$)Y+_osSvX{l$z#W_VAT5u#4UZWPnb z6S`j)7}8WuFNz_!bZz_fM|rf`p0mN(pnKowokNq)m7_THdJL)OY#ms-zM$ZX%?toD zU)OljpdBABis*0WE7M*OfQZ<+0)1bQw@o*upo;t3rH?DJy|4HW`yT2LwleSe?abiA z<>4)@T9jUzXf>pxiBy{Dd?a^^86WuAp5 z9B#_1VBEV=GwSL>H9x>;^WPGe6K;qDt_3F;qvaKtKB2W@+i)&=)Ze(guHn~}r{XBpc_ zHKW*)re6yP+Y_lf_!ZKuQ4?lMy&tx?3UO^kX2raXE#3Hm__OM{RCO(q`ezladIf-KJ1!cw)&i&p7+vlGpO6K%!OH)|zz_XzlhI=7(m?5b77czgu zw_w=>A$ty1ZiUCJb2~A0w^qRE{Tc7(d&&%ln$M&YC#mIP#g5sn4UK^>aF)`+in$+e2TgCr(OY6er>|oB-DjRnJPe5npQ^z+> zzzBVxcW)+#{(E5;r|Wc~cv)}xI{?8hYMP#ACo!Hu?`(AcJMz)=4}ks0sH+`MnYx?5 zCZ`3j`4Yw+H5dPna56j+@(#j{eZ6)M8o{gmP4Kq856qyeMR9RG0by<7AD{WAaM9sIX`DT4fL z6Q9N;{Zi421O)jo5@0Gf`^6G`(h8l`>G!Vnb;B$ysYA5>uo9}vBaRgDc&#w6b{-}f z2-ZV)JoQ*(aRl3(`OoV66Fvkp4b)$D;Kj=8o2KnX1od;8A~@6Ou6IO4nIAvy9~xg} zx;R0%GdRB8dg(g9$>$t!Zo0ataQ%tKPlr6X`_)HE$=W2VI23!yPfLM|L(~^pAQ-5c z83AbL{~uRkkGwgcxS0;IDun3mD9~TF(dDx4he4AqPuXlnq(z<&M7Dtk=5(y&dNtmW zc9)j*fL26T^P$CIK~*Q_WLo{u#IjBMV#cnk%LjwTKNk#I_m;_>|7ii(hhH~4U4j;< zR@4(8R77JV%`Tjr|Bs*XpM9cgfvnLsP+I%MGJ)FFRn_1%lla2=dPN3fMuCd?k(43U zx)s_A3?y=)B$`*#@lOj-K8qQAI@C?}+UP6#gN-h%9zZI$%^MGQyMO)$gy!krg>e8M z1%+8fK2Ne!J2F%7f(bv6{P!OxHU%iSn5x@iR8*P71g7#%L@MIn3ZbNdkrHujguD$a zqzBlkgs~aU1b|Zjfu?x#NIx6JLds92-&+y?-x&m8CUEj?u#1r)&eU;7b#!!S_W8Yq zWSi3~RCJ_s#p|3f>UIyQGaY`B4IDi;T$6G5&qIgQyp*^zkFyf;9iA_lt}7tq3uI0) zS8kCTSAyUd53P(-P_1N$(Nwt3{zF>%@8OAul=~ebK3jLtl%En;#2vfgm_vR3W4DpF zFiYW-yi<#sgYX>_v{jP44k4i?7)OB|$SI;_pamYSi*AsoD)~&_`2vjwD$zV~e^GJT z<^6B{?U>lfkn}m*n!!Frym1q!80DUF;%=XwIdJ6a2}3vI%po3b>Yp^vg~WVP-=#)5 zl>AQEQ&w_8y`-g7$Hf1EpnKYeDUhJ7+!VK8c*E23{{h_g9<_)8Jnmp;iw zO*+IY&3IOFR952 z0b<;fSCj5}h+XTS0)C-BWA18a+t;<8xy|#6Wwr={< zE~xQy_kTDGJvYq{J$UYVCwwsxAxR!b((|GhsG63Ki)5LZrF?Pc8xFO+^+>%;=XePJ zo%jFrro+|u32Vx6qeG_>KnvFU{mHFUBOUaC!j`c@n34pX>fRICsQZXB$@(niLG`ws zMmQxvckvoOQk%1XeCmwn^q3Lnp9EV(G| zPRx9z;Kq)2V>M{QnFL@=yv|8Y&v&^xTL-C90hc1VKNcqQWC|E*OCmJPFxV)E@vwB) zl^d>u(v_ta^9Iu`;z-K(Cr85z)Y0-cpySVCLs zU$7_Y@={4!iKWkrq*=JUWVCPrjAV;3eUeYKDsrHA}f3LLL19#-UG(9b9Z#nhdAXCmSy>CORW z7+G6QG%lcvNm?dMecpO9vMr3nDw@};gxcTec|D!|{a+6o=w6Y*%23e-;J`e_y9MHB zS*Aa^7aaQdcR2&7YWh@f!&d;G=!^VwLgAH&ZYj<4qUB_$#{EzWX0vxxQ>RgJPaITO z&vyzansn~MSe-nP<)qY>T5dX)lCnj~CokqHtbnu&rXut8GivqY(HpM1WkYwYjfYE- zi|gexZLMWCam9{>Z@ZINvHH9ZYS9|R?C8vAiu@)(xhs(TC9hWm6RSW5t2;}OG%;UYJZrqix)tk>+lPjs` zb{v7;%zpi?scMyUw9e1cQ{oM=*-vQt*`@5c$hdA-l}u112{GfLyK0cZub%2qbhrVVfWN5 z&{hy04t7FGS1s1a@dTBCT~PXNP*t$lorotQXhPc*3%x3g5_XmNtwarn%!L-Q3d#T% z|KcYN+&PtyqDOS4ejoDT13Q7Z(KNd#QYy={msFG2)x&*7*q;awtJ|VZOVs&DCzs+g zzN1kGf1;@Kvg#2_RV^|XSVdn7sY_#)T;$VQHBo3%ezIS#bAzZu>#3~N@4+bD#Hg0& zc}%kl%oDP~{rRy8xY|Yl+lAdx0%Y%}qC~Dz*if}UGI6EvCG#_mT)z#JxKf$$%o_7d z&|WlCm{=F8H=VwxE|!r^=8xK#J&=+Kd>xVCCL6_7;_Jy<9gxI}9yIH$P&)GQn}~+_ z4Ls!y+jz!tNhvjS3-nlN?w>(4MY>BA%0(;JcU6aS)c1Z}Ke;!fekakV!yME8`8_|o z|M%9qtiHJK!Nvw~(0Wa3<=20-U7|b`6}!A>=#BMcht~EcC5S|WGXATdSIq-Y+ zAHDcgJxvhvQ$L;4_3?0?lWHK;_`oNB&O)VO<=`kXp?s@_%^y}9`*Vc|%u-}n;39~>9*rZ%a>T_Ln_7Q~n-9ulU$*fw;bX$AL; zxVpEbD!JhVacnKeJwJP?s;Wncf0C-d^asj)4wb0Q+Nm4fAX<+M@_;X9ni_G!Nzk6+ zjVWeX!`(zq4r_NArFvfhw+aJ`ap`8AXiUy|JastISs_E$6lFT|xG5uSvvT$$yu;H@ z)7;amr_>%Wm17(UU@vRW=L(ie!rB8HZRottI;n4zv5pA}Sti9(5Hj|8+`@plaVGjf z!Zl`1NCs_7bc54CheuXYS7omji2-3i><)CZG|*H^;3(L@1>4A^+{MxQmh^7tZ+J@a zxu*K(WvSTAURJmtkYuExZUrJ&+_yrTH?Rc#CybJLr140-MBBx^4*MMtQ0luVMwQC| zz9XQWo7l(Usv9}imeedX^U3W5%@+K=$kkI)@rtsN>`!hRr4rb4(~zAwQ{!Cq@TSKg zAyO2x57lTjjZe@0H#B*`H_yjydXfyQ?8U<~GbycK4F5oYm>1`U=0ZO} zikQ_>!_z3uK!%l+Jq&&!BFEIsLJ!T?Wn1bW=voVfhOe7u2?#H0evZsy8mRpNEN~?AE~qF(mr~yyJ9@g8qrX&jCK{<-w^n9zZlo3JZvZ52HlP;XrTpIkBm!syfK-qUy-84-T}=VMuP5_+Cy zak^e!s;nwB4|YNyDDPxO%Atx1fYnLeZ91GaGd84Jx;`*7yQT)${tC?(VQ2oDN{ShX zAm`K#V0gCfxn&7#%NcpR5vw#QYVOB-#qgHM1rLZYKU1|_E5@af1gSj*DS612DC=*M zPwYa`jyE<%fg%vK^B{BgQhiFp#mJt==1H+%OGW>xj*!0=Kf}qxzE0muLE2x%pAs^} zhrZL&6a~lGc=5HI!oGK^DM!JP$n-=ljNBlOa~y~qo9`TBA=&i7We6XDSrD^<$^wXxy^{Ts$hB&L z?>{wA6+$y`R$UaEoR)KR2@$M&(-5=ektVjLE`8LPmy9DM6J@0$wE8P=-brRbw*8%q_Q$fpS_;Tku(b;QE9Ez=T}sC#QDmp z(GhtaiBz_D8deRye4$D)K-9z!0!MqKEB8_NzF_yAXU6O(sQ~%8=P@0?%mtIlW-FGX z;jZ27!vh}BS+hgnRaaSCH%hDd$nd?0;*%w%u;FAov`J3Yoj+HlZ0?*eA($xK+k%7Ja3m3{ocyD$x%X8fOT!*t2yEy*u$U5NN30 z|HXUDVZGzzP%JqSg|RjS|IYJr8Bg5_pjbwmHGAGqEJPMD%9twMZDl~2E)RRn1|yV+ z=}>mN^lj5|@9)>UNMTC&Qrs3s1WCNBIfIXoTt0&VVvE)A&dq(`@m}6fWI?9UMvUX4 z(&hNDu+NNRovP8}6xO!=3N+YRZ(mHo49)@;FPs{QRa7KQtVce@v47)F@r? z{JMihd(g_|4`sDac(ThK^5QC~NPPyj6{?!g-LSIfHB}msJZ~zgxeETJQ>3k)(FiG@ z-uB{fym+}O`>1?4kdj&}QnA1%Szc})MumBT4Ocq)&#%~Hw+8ewRh(==s6KQ{m4KLq zX*rX)cnAqp<$m&oxX_Uylbi!eg+@#M*~eyVYf@uy_i$!PWScF+ekv#90-;QH`GzTn z{m%_KDZxM$C)5yJ7qFXUNNvtC(zQ z(?v&wCK4ihyxsOKgq{htH~E-7YGB??X35Ct(!V7j71>}B*u<4(_(3I1FR&xcin+g8 zGnQfw1on74yiMExB%l%>dnCtc=Gq~?Q&j^PLcfET+@`czF$;QSP_7@k}Vylo+9y>A-=F^5YlVpX?3XXYJU^K43d1-d4kF=}; zPJF_BM~^GgyTdM{uKQvs^(~&A!eVdvRXhRVlnC8KdX0pYhfu34rm>^hkJ-!f;b&Ky z#MC*$CSg^RAcWE764w%vSZPd2ZN7SmmkoE&mv^5JUk|UzucjDj#^YHx4yR$R)Z?*F zB<3G~@wvzwZvj_X!%+gHykWfefJUr1!^4ha5(9}$nLL|K%E%ub6 z?DLO}2!ISF+*Y2O#5{pt^2 zD%d8r<)3#R;CqXa(1*RT{lUk6g5AGAKfW~K8i-t-++cqWAqT&UsOCJO`5t}QAd$iEH@mZ1Vf((L=&1bPAkioH*}tTAj6Tz zm7MA?%dS$tD%-RS4sWSUChd$&kji@Pgl#IPwqG~-`0UM5!G3@J{<`z*0HkyKl!*uO z?CyuC%Zwi$Y*qQ3;=Zz+GUk?Z zKYh0w!N9H;A>V6};tJm3_sx{9sVtrVd&EBT<~jbeRG^>B17d~Q)iuqo=q}}7aYODm zjNm~+^aC@Kg7V~xE}~2Uc@Muyi7B2<2e)L6n)x)oT;g4y|VMAQvWXs_C`{?Jj=(k*ZyTK7Vq$3Sez+3_rR^y9T20>@r zb_YWC`_XV~8MEN?KAWRbE_A4?-{T4X$L_3w*i3c$eiM&4_h){Vw=>_rUHKTq>P=W@ zpc0n%?FIZt=wa`-#_SNA|9TXY;0}^dJ($lfCmfHlhE4D|;w(ZsI9=<2=ZZugHc6op|o(G8Io6m(Cygz^C2OpUCg0U4E=)VCblYR?y5DR$YBr}qj zESTOj&+eU;6N$<<;4JSkDQ=JBzyE$tc^2x`}*|T=rBM7INee$Yhr_ zo+DqUQjt~^)FT_o2iY)>q_^MZG16d)^eYc-=*^iP;!t_>H3AiccDxAeJ>#IFG$?}& z7|s5@1@XWoHXajMnwi;nol2+V@j$EC$jp7(7iuHx!J7n`~LReYD25zdU^MoGv~>H-#K1{2qY$ zz*14)`NE{GHg_ZGX}QA_w>Hp7z2D=v*Vl)5(lI?cwlxVPeA|62r&F2l8eOo&e@XJ7 zAAU<(g8w`|N`9cN)>Vzn9J@I{yc6en?P+K_jMsWh(oKNhW_|FEw_NaguRG!m9bF-D zk=#!N(9oG4;+-^7aR4@<)7J3j!C(RLy9h)_Jz?P2)WR@EDf@pv4Zk}dIh21=s%6u= zRP*2%dGoRAv1aZ4aPVey2BY%>zi7yhQIDzE97o=qqfhm8DhhU>eDlTcMfQOAcJ7#* z12nVPSYiJu?9tqwy@PdLkDeerPH)za_CHv19@zTf^&F(>Ck+J^)j8kUh53^2k9%js zWq9K}YY0v%t)f@BR+)yoe=y8IBzYJjmBvid^MLd7bL5P=adOxnu=W8m#RE=;>3+#V zt3K67rnRAvbE?nvBgb99@0Z0d6}2-%51(6K?Sy+F@0Ii+3-#+Z)Zh+-^qBOuqVe2Z ziwZvpKQ5vX;b>64iQ%o`R&V>Z^nA@8?eN9Y-h^E`e4ZxZpvvLjI|6qzvoWWKn_2>_#Ia)f9fGxt47;<;W*m5+q$h02do6 zxp9!JXt=;QZCP?rt=%5s9K~rg8z<3n`ELMJc|547KA5L_&=ea31qjw<<1crCV$|e8 zir;ld(#}RC=`w~1<5Ar++~(~*iy0m>XY*XiLcreGiaDx7VQ&Ks`#!wq*z)pljZexO zZvgu`z{*GlnD!JC->+A4HaxXb~VMmduV6ulC96?KnrC?O5X-y_`5` zmizaU%w`)C9mDuMtrp~EucGrOF6R3(Ed(pQ@XE0(BB{`~@W)Eu-MrkfGy8toX|wj% zbAwR-dl~%SNKKo9A;NK3gnP(A4iRjOcv^{_+=+2aG8CzV-Iw2jE9gPPLht+i2D@{i z8>M;&f8b&f5I$$L3U7FJ3gMR$k{`O84l?Du;O>kzKPOn7J}W=@x&memo*kCH9qMG~ z4m79nm2;y0M03Qo+9QM|fe5!=-LVXvv@D<(DV*hEhtL}O0P0S|wJ}LLb+Cd^V3g4} z=vTLgr#+ggAy2h%@WqEGOsVtaRhgFf+JgtOJi|irBnj3x63{xD*jrcxp(zO&`$1HG z3(1B<9x?`VaOxjnYPT3Jm5*G7Fa}@xOM+9=XPQ)s@wi#v z7i%a?qknV86ViZ{{=4m2VUkm&wpP}`PM;8W{l)vio^Zfs=Z-K15r2T-fiNI`a~g?Y zEjq8xZ>D`#+*#+Fm%x*){vdT;AE8Fyz1kS!z47AWr!6vLbPcZ2{JOK_=mSxIXuW29 zCP%cx;5laD@6lcGtX}v!%3-0Ok*Q-+Pe@uXCarED7GWozzkNoe`?dqyy)$T##EshP zL=5Z6CMB70Z*>EO5qfiwrDyD+xT_c`&meqN4QPESI>eV8b(JafP<2!?$)J7Yp_>brMyWPh& z=Ib$n4VZcN-dy(3( zol)pBrGVlN-3mLOMO&t5a`}} zf1!CZEo9>NIbrQsq;bsR!Ck_7uy5M%?sHh~vJhBe5vXs+w7Ihr^fvpB(DyRT+5P7U`Z&2QG<>V!i|=ecH2^BK-^)x-FA(g;`T&%G*%;ZpT*a?bLeuxA zCk+Ou1g~+rpX5IW*BYT8cD=FO()-d3Ms6A`$AA2LTO^w25F#>op5+5)G5Liv4u8?^ z^qQO6hsXPPk_A;V4Zyn*PNX|_vTfOUBGzfIh1>6d$+li6hCLqRX7@syG=LZLu&i!2 z6m7@j@ew>-X=ahyiHLTX+WPN&+U3gyZX2rbNVkn#ZA9x^(XZW*%`9dc^!H~+#sM?G z@!a+JydJ4VTyDeGmivsvIkmZynZwyWR0x{+h6oLDm{evjka~DPJ+_1++X$*j$pg=E z1dws~Ul5z!7iZz73L(PVpTFd-MOUp71(b)gwQoRRTi=q=iuRZ@;=VY0=F zz42d}%J*m2_2P4M0DfBlyR!u&lOSioHzQvdy|#aA{_)BLSl8?&gnilhXPv{e`;J+! z70`FgzIQnCrK~oDNt9Elof@8fgkk*KbiAn(gALrX`R36ZXb&S>1uoC7R#MBT z^eSI)oLi*Xu8hwufu136B)GZz^-Qiro0R*V!yLmnB$Ccqh7%W6h{D>9uj~n+Nx>C2$hiA5&Cbj6Sce zMUR)F-k8Mien@CPx0n7Jhwqzgy&qe54zv1!R&;c;(BAJO!@ zUNM~;&4pzz!FDXDcO6%6q(%%Esylzyg!39jZ##Zdq$3#ARGue6cMQP9^{ z9XKmi-n)G>u?QodsWWOY>V?E`vK&a#8nm?M4=QS6MR)R<2wPWM^V~!B=H_dtwIE{D zXN^uS{zM9o^}~eXWjf54gR%{;JEMt`FzRes`?4)Kg1o5R-gJ?B<2%Iw0Ja3 zC9|GZTgQeP9px}C9%hHT+zurE_uTJB-(^i$mU_&b31=1GfaQUbuFnFK5kW$zeFWp4 zlL5Q;iyr!`NgX^QAg2jm)sa+|M-iMZ!v5i94{Ml*IhK=XV zlId3&mT;VxqTO2joAJcTSdOSB&!SFl-pM{RjLnT^b^m-4!|61wIdZtvNR1MMoGp!D z&griLW&P|C^vD*Av&)iK?nvo>F+mv5Wy-g2vN5bu#VOZ&ag%vIKGhFso>BY7F%*$& zyoiZMp!5?3(?N|DCb~T#iMLK0Yj>xq?e6{34+&aE!$_Vn{F%URD06kYra|QlYaioP zT=W2k=}?8&!K%^BW;Q$xb6A?(o0hTH#acHhC&zc6PO|j$`+jDsP80{0ZdG#8=n@D$ zG(RmdB}dOw}r9s+(>QY6P@IzVa_8Ma4wO3Rs(HTttO zH;D%v9=jKrHlXzsI`QVsRRiQXgtib%UAWT!>v{c`KbZ`>p#glw|5Ly!&bQ#IckNKa#DCkT8jnj%BP}FO|k2 zGD)AZ&dlK^9um(S*p1+7Eo-6mWivp6JE1Uwn$SE;HOhG4N98mcMX#Yb4w#420-OV!wUwG0rx}Y5$F=}lnTM^U zufY&0{&vK);wsfk8Ig~jrH~t$)Rrl3Knab#BF&WaQrUfF_7syG_WUgsNieq()IB2wJFKG$#!JS`O zi;dNJJ{3z2u$gf(f;M$&H4b6J4*o5=2Y}EmCI0qly#o&)ThQnp;!Pi8xQTMq{NR*8 zrV0let)yuP;-WSeD4OCYC1@#OuOcp$o+`*CWku6K1J}^ZOjtR?SK&!^UjRu3eo4spM3ri=Rm`Zw6XRLAGa)dl5svUA>ZUz_}u8W zMA}qVbf?uK8K5Xs9^GFiZ*m&Nn=EFVzTDJkI#i~6D)QL8MxI{)wdgLHIx%0*i*)86 zzqoi&gXK`1QRgWpJVTPPE&-cK0~!Y><{ieg<`1gDACv-eN;cAz0W{XJu_d1YH-e1+ zZR&gPf-O1u@QuqNQ~-FH*?5n9xN~}9)CD{#~?t) zAYjrsrC-JIhx;2Rp6m?X&jNFpR>HOLJb?!jaJRP4KiTayuo$M3hGP--YSQ_>`46Ky z<2`hjL)t?-Q#y_M{!j4WfU^~j1<&1x@wgb0TbG1RH!4P$F>|=BZlVZ&%ZDZX#PG6C zb;A2SFFKsd47dfko-^!$J{mPEBD6TmZ9Z@QKYh1Jk1RNWtGv!I1 zgLS1YZ3g#1hvQ`D)*s>jwL8=8Jm=kAX!I6@9T9fjs6{Ok@IVi7Gl3AY8|4n1=-xP&4=wsshxLb15yMEz~mG{VK6E({e~ za3|MJ(8u#q_`F^tY=0Q^uK_bY+-KOUQ45LowI=$$Ph%6)kAoc)koDw9&b6(o2cn6+ z4#z7T<53i{K>NS2RT81Ty_Sn*0#OVe4k^?=9_?kv@}#op{Ga+j%v{)DRb~YSOR zyVRpGa%uCd*M$%&6uZpacj(D+<%ywH^gpT_DcHdy#$_O>6ind7~YFs@oUxcEFwx1Xt&+0`aNBvSv*bwoLjxcf>F{k!K z=3RhcBs)?MxAxDu zFJ@quZ)L=)$ec}-l29X9o?SB}jLOD^T9QelO9{i!MW|)f=A^KWXs$1+4Yl^htFsEM zn78lW6kN6+#gWtZq+oQz$ zNc-;&gcF_ExiY))4&z!LCWl%oH2^T;8@#1BZ4d{`njj8gdcL7{Be2!PC^yO)@>zaV z$x=tTKg96bl}@I9FztbGc@8Ps{@lX^?~7=APn>a;Ku4s6q|aD>eIjc#Z3sSHb@gr zjjdWvb4DtDBU-3feoN&*1u;)W%t@Vf6J^&nR7;C1)kfU%c(+$d^0)RM(u#{NstcMt z*Hq9dc>LOS-`RLgu9d~gBN4&;v20H^8uuQrVm82QJ_ZKED%BZ*u0)N6{MUOXtIbHi zI?&g`Ao|B;g2?hS5bPG{Dz9rFVk(o5DE{q+XsyYN&8w?}F2kMhx?MIST$Eg7E0-Z% zU`|}Tl-uGIN7H}Cllt%o%L_jD$|=|;aQsie=?pH#*eQz;EI)656iqDyc$t7*9L>)bq z;B)(KoMQtQ7`Mx{8HND9cFX1~tTY^Iru-nDn0d5s>`D-=1}IWdal`JrRH&%`nkpJ65zI$T^L zOIo`@kdmY=NUM~>D4CpZKnd}bSrMb!(Q55ias=S0-Boz%hw~P0tuh9^cnv|e=>e|+ z(ZyrNB8NIh7)^>Ea9p&AMj%*@-F*HY>@fqjxL2LeK|dH4jcI;tq2|J9oM@JbE>CJ+ z`c-_om7+2a;rOTD#rPZ)RjjG08F60MWjAoaf=Q*Cqc5+ar1bTP2Qt=mpxv_$5<_J!G@GL| zMsQ7J{sfiRc17l!sAB8Dw2;YszK!u9dN2+Rc!|NjOipP{)992YEepN*Ife=YrN3mv z$M}~yh6*49LYRMuJGswoRe&%*36qnU4~El$>uKpvK&7%4ZKX$ccSp)+0KT` zlXb3gAzpQisz+Bw@)87^PAWsp9{JB}h34>vWFGn&uAP$P;ZAig?b6s;!qN3dpvp>+ z7yL6_63d*HRGa()=x!dRbt1BLZuDHk1)yp`V}vvir(m6^e$#{XVCZIvB5QB*qRE*dp$O7c3G#D_ zkz0VC`E*tX#gL3VwlOvhYcQ;qLLGH+c$3v>LW-1Jx)tphjpnk1F^pOW2QuP1Mj}bS zX(@4FB8KymI8gG#dGBYVbO!l&>p`s*sid!UWU3h2U-Unr)iEi7S+S7;N{g9LF)!RW z{I#i3=?88RS~{mL(#wB@iCjBOK7ZxahDgHJ0>}=@Rfy4T2rs!Z{3pD`^%$oXRk3Sw zzNBia+m52d?jap1R&)}p!842K{UF~@_0zxk4y-MWdo)i``TaWP#uBb*iIF4&DkeFT zGRFOqpu(2D4jI{l>RI{D6V`;UcWs^q=6P8H)+@4rECT>bkV-%VCa)uVUNX6Rh(Oaq6zyo zt^pE=CO)wS5v($NzHp7h?1rNJlJ&;aTQS_)Vgdv(4lY?sVm^UxX>={RycnTX(&_NV z?=_7_$%AnLQyY`oaQxyH#D_lWvRh(B3Ym$d1H#y`IkOc>BQreoSQ1f_kQs#XHprM( zj(7Dq-md50QbdaODjlUl81x0_PGO_p?A0-SbP+<%ml`cGXUt0e66VL##Xm;OhQ;?? zeb`^}{XL+bY1z$PU(YEXpsjEEE;2w2!%X}g`x_-*@bA67nfs@h*<w{ax2mrHXr)_tsdqJ?s=R<)T$f)$pR2mg z&d<(h>qGz%T$j|LRQCPVGYkD5Md2sptP5<-F?{olMrC-?0wRF{k>!kqR~Ob~fseQF zrEPOC{%U^VaXP%;=T`?g8U%EEXQ`fYT2XE3>=XAe-~+`? z58oCFv*ASpuV4GsSIQbo7ITxXe}PN2YX<8(LrB9~H#(-1u&iMWl~})V>(MZFlpi+k z!FAu}^=uheO|v>zG&M@xSGK9E_hN0rR~rj}%e7h>`V-P+b*9S%{ZvQw=PP^lZgI|Y zk~xI+CuFOook(b@>1M@*`gQ4y_e!pK<#l2jz~?Cdhj3SC`%Vl5EJ^nFi7O?cj^kmH z#oFLyGv&6Xss3na&lg{t`Lz zAYh{NV2qS3Id-@m;L4Zxe81dR zO4nP|42|?vTOSu9_G-jeh8m`t(s9->R})euA{7PEIf>}6&=@tc9_TJZP7+RlG|bvI zWDe0tQEP;6QSruI5ASf1b_iXWw3I3zq30o-2(GSw7_fl^p2)|mna09OtE+YNu9B)H zI1kPiHPU-GT9(PTlgb#qoK_?eR3!IDg_gDcBdZ=jVYkI^TH`r-6nW39dF_(4J)c(o zg@bNdl88bPfNIR#ddQZbWY#t)on>sr@D0s)5u^6UN|XG%(~skEn49H}aVCA;en?3S zRXy&j|EI67Y>2A~)=Yv22<{$&yE`Ok2=4Cg4ucaMhGB4bcY?#(A!lC5)FaL--Hf>0L*4xk z#9Ai3SG=jR62(Tsa^v(+e%>7kSTxOk7C1G(6j%@C8I_TaG4DPb=xxJbzS#u-D3$e5`CAAWxX3?@MxPA+9tU83-v#|JKYqvAh&oAh68xTjP&t1o2Va zyg#>gD}8BuGZe_*3~qR+T10A>PNJc?EmGF)HxHTZg#bumBC}JL@qo&gHy7dIo}CMo zpyQ_-)h#%nFkvh2w@w|&$WJ?|+^Jz*cXAHUvBP}>N3P4eEw7&QoC)SQS4t`$ZH2eDx9cnkXPVfY&I)T%*RJLH?yE|(XNdcIAIU1_etD|vsg+%j5E~qsZQ4{?6X)5)~0eiZ#b?l zUEVuT*NWe$t!W8yGc1E})Ok+kP78VFo1_#(fC`Y?H!`^h)q@uaN3L;{guqi_ag)&y1aiXue zSe)Y=<${(;yi1ogQ(9Zy2ReIVgFv3Jex+j^mpBZdw$&Q5F75O~)t<&SzF{Jzd(~mp zeu^O4+@F0+u8QG>E)9w#2gIHsspZ}jQxKiyRB$B;<$;$~%3khe!OTHGaXbPp_9JPY>4Z5^~24qWBWO_JCKcIB&c)F1s5{Te7Te?)gwAX8Gt z14YyJt3l%NG6IA6yhYcdfbvSk5kRl`xX!YOE)M>S#cv8WG~Ht|XLPy1`IlR|aliu^ zI6EMw)5E1?IaCJ5ZYdl<6EInACAwoit94uvN*-k|ee}ma$3Qz$c7Zu^J}WNzXTaDd z1Zg|J%xje%pjMAlHb))?VKql;?yG4v;1dbxJsemd>Tp=(ajvmds^%0urov;Dt2}os0V-X((h8OkknrPT zlTnDSUKoh%`49XuWsG$Qf>cL!yk24I+vlkl;eW8kgajdRU;5Yg@L?tUMFV0^^ z^!ElKp;=fQ+jytWxR20y7bl5ao`oo+fCcPL=uMl>A=fG#!KeA#Oj%q6wh`~o;|Ywo zSvCP3fmLbr(itQ4vj^sy!;Rb_S?+hdgXl>%*9|Nu?2_odqH0YO`HGeVYauUrP@~F- zM&KujsS{%8Kq@9-Ou`ZP$Vmzx016 z119(Z7fhuYh2qg-5{0BKCV|uIv&BcWUPL`tYWs?6f*icm^CCRQe?Y>#x{2Z%d<2nM zvGau+-SrVFp)b8vageL4^5BDQbv4TJ3ObsMkV^mI5}YWZN|@v;BPC-{V#Bw^UK8pR zRwA;w@|tTSZ7bIA>ijm8S3^{|W!J0qJvn&q(zIt5E{mp;lS zp)LdTwT|V_A_2I#2R&&ph(_$c(+lXMsyi;NKetoC33#*CjZljLjMcbyo{A6;XC4n< zjKNhe62eTpJclL`@RH4R<(jl!Q4FN>DJ^|bhe%t>zxQ#Rz2e7ob$C6;fbnW&Wbb~C z)HBC(7j-`b+S@9=I|L<;Wn9I@V)SX1jq^K%?%d``Gok7r?UQ!(`>VF}EFvqSK#d!< zOk&cM(iZ;3cyrJ*39oqJx59nuF^niNjZ(CdvW%*m~;>2INaB9)qL4=N~AWi zyNJ*Q%_k>hCVFAbe1B_(fykzb(j-v&qL2Mke!dDCxov{V1#4Y*IztHa?-pqUg?ax} z$gXm1_Agy0#$#0gfeYQdus_5Q4_0I*h`V?FQqVX$EZ$1*1g{?K!;G-*%VK)oZF=$P z6KkKO=r3hOmu`lmASRJ6R#b8hk{D0%GYMU*s|!q@-)NM@_Szbxv->1!vmC)g)dHqM zl&1|Gf$2J{!9BXnuW9e!--+FAjBfBsgb=DO5w424ITu`r{G zOE0es(mm=3DAb_beZ$04Dtk^<6Mv+tt6q7iu5MIq#d_*61rsqmo-%lLsc)(`kIp2dA~_%mzHP-Tpis- zfF4rAsl+Zv^HZIKrd=iq5)*CtIOSO0Tueu?Tq5=5%}}cQVbX)=pz^4nzO;^> z)1F*%w%h`RrddLx6Rn~`uEwW0syNmOFEF|~*J1nOPB9-Lu~$K!mfYlT986yW482TH zofwF_8T+jMWVG%bhi zH}c8c2D=y-!_(us(~(3-fXF!yE$>t%g3=r_x0OCy2UxKrPWU16X1%#lEodsCrnVln z(bEc`UZqvpsIGoGjO&UkuFM~8E4b&0PQ62z%7hHfqE|`8XBXn4cOQy z$uE8?!HcSW`Tw@Izl=(6VM($&cUQ$2x+PMs-a+Ia)d7+B_48tvN>LwC>-jS0xquHo zKH=ox*c9@;SZ_Q@Q<|KS?=G~G^~oxv_-3bT$|=ips_X!qQF8FBtTo<|#{X)e=iTNU z`4Wr@4;`Hv8<3B2Z@64rv zlYgA3m!ix}8gpo_w$uzJp{S9X(haeHFr<=QAn;SCX*bbTO9%WQu&t1bCoxT79}gC3 znjLfa0OjAYY1W_+*_x|ZuV!aDN-Q~z4biHf+LhD8?n+7;QxRp>guF|EI?a|7MoL%7 zFuFGnBIX-$?F$=5T~{WEO%bt)N+`w_H3P^*K#)Kp4-TtMcJ>?foBg@Y!f?LI7wv=} zH8xE1l{{a3o!Iaj9G|`*6w<@Bi2byA`g)@>+Do@OuJ0>@T=DYY^75xfbhKBovdSpv zhdbjE0kWtNr%iEZPcou>v{Yf#HIkKUS^wA)r?PBvSP&4KG;ti3+(JI$&I&c;es*iN z&IaCtry<7akaC>5`%sFCR4CR7rb5i`PVF-hLYTJ+CGpZeiG5? z=-dk|l`LwBwLo#cjHeZI*$JtOM-?7Xsg%u4(z* z&MW_IucSI`_sfP+xH;Hz2G^c5SS>GGT& zP}yX2e-lxDbo4Pu$n%-d3f^WL66&t|4KuEXP;tq1h&j97+u8Kk>7Mlx?c>ry`)kw1 zgIpn+p?rZ{nRY)_Txkm2H7_fzJTB_2_Q{8-FyuovP%f;FoNY(4mc=bb@zx*vKJ$A} z!@b|#*Ze!Zg`JThR357kBApK!Nn|q)@=tr~DgyJfBa-h!zqrt5DLo;W``?%T-tOuN zF2tCJcJ)}w?={)toemPNaQFV{m+i{`GO?XhwWa6;+zeWy z!_5*%)osEokN@VwQI<*^WEZsHx^KUJpWuC0-92$kXZ>KFH;yEud0adg`Tp$H@C5>K zaEIL`J&E<#X;Yc03sb$b93vJz&ek7!&pcOwcB?I4EM6AuS8Lk)W3`$uyP3S8HBOvn zLx9%CL8_p?)cL_PZVarwa%( zn03prnaodCrn`$?8csEFy=C9o-!CShrlLaBYB=BYfAukT3d<(auJ!F~vBbpM%WZ6w zzZ@PK>+y1U>^L0%fHI`lgEOKouJ$*cvW){P{dv=tx_#7XIfb$GxP{^`fgbAe z(9WXMtq;xaLEJ`Y1nR*kqYSb!m`i}ACCOemV=YFHFbbo6oeoxynfr=)T8ZqLY zBw?Vcy5GVAV?AqIMCRZmW*d^Fne{t4U~PIsG}WZQdrrumE80r z6-1;}pOqg#R{^bgdC}V5srAfzn6$TXMPS>Z0W7%ad}mp?-;lZ}e6*iBvbrn=Vw00M zd~t{2A&fKcUKM&k_@nCeB{8#`0&cPN%;%bi|7oEHR~x4%;(RT zm8l=Zr3;dTaZWLb^DBr-zmV0aM;eyp)xgRnEk|GyT%qydI|!z<=6jIW1;aGGsOz1w4bWdof{m)UK^ zMa{?!ed*7JNrUL)L7cwj#aJLZVj$fNmLO@9J9mI}`x+)DSF4pY_e(N-uUL-)+a`;~cHyoqc8mqCU98ce~a zeAT&*&b^73hIfbio7cX+h_u&DF5lgHKkE#z3EuIf>F##s-oFzHc^qhrXt|`sY#To` z{wZ?J7~>#1;f#!N)K^ZBg;{soya5hPXsw5ZoUKQ;1;si%b7AL}Rlu}+wjtC=0i#y? zi(yWQxTl0d&wWzJPqpwT))s($={{*rnu4zFRRsG2s{wXirf6+G?qs%ax31Pa1I1+3 zh}H^%vRaCMsjAo;BNRb^*X(it!sEkaJALQT)UTI^Hv9)Ml;EzbuiEw=XttV~;k(yT z8=DmZ=~zvTXWQ2oL5QsO^HLHD4b9l^m+iL?3mk|S)2Wo(a$xITh)63e6b_02>;>Q{ z-y!Yk;8IhU${S2o1IE!A^)&J-%8i$-_UA}G7H-_nBs|`ksF-zuxgq5OV1i#B-2fMx za1Q%wTV;d3}*#(UJ5uKU z6sS%ptM##!bU6UBY$@9b@0jitNi?eWsE5+2cds zd|T*X0Kr9#P)m;if)A%b%DjY`+p81GmhXGl`}0{JO8YIcw)3W;F^bD(J{)n!6xav< z{iT;+(q~8Kgm)#PA;bNX+?>h=o!;YD#uSo{Rk=35grC?~)4Uh3MAz&5Dj{G!H$ftu zM@DjwuR(Me`5ziFgl5>GeARR&EaI*_9KXuT^jnmp!Q+}K%M)uneim#6PGK3pKJ-6} zaxyPB*(R6PK%=Q2w78*sOIl$e2{qsqcLBAP^A(3o4(V83#8KPZ5?$KSmu$ZNUu+M+o|Y^9DD>NBwISY8$6776X4s@?wd4W?+bC_MFKV*ziIc4SVuF z9J$_3xmZd)T)$6NT$^P33Jn|bBcnJ#^Yy6ERR7z#adHr_sqviFbWV4KAxPv7@Mg>_ zrp)9wM~CI@aIOH~*$a&8kj=npT_|hexYoi(>8*pxl)fsculRH6+Wl>3LZKRG=sGKR z0a&Ra*5Bi7I95E>XV>H1``Dr-jKnt8^*$Vy+GS-&$EpknwdbHZU3*q#S;5zoeywr^ zlQ7_=1I~I;zs-Kj3ad_qg=OIx?iM?~X0GJL@qElSL?B`ISPp1RyAIoT3P~@C_{@K8 zDiBG0(%qc4&1tu4Fynu0@2GFwe zteoB8a5I#{4RhP}Ar+Tv!f4t$5IwR^wsuOhMeyk6(otHRsX-ky-5+RD=TmC};zQHK z)7q8=6f_5lal`*jR}u)!q9T~B*)!@S{Klw^nOk0kMKIqM094Be0KOhyECI)}Os{`Q zbKmpNoG%15(Wz~>n6PbhBfZR@CtS%Kkj~jjZLQP&jao|Yqz=zfD| zv9j%g;LK;a44p@@6Hh&BD$FZ_!Rrqy$}-f#v;FPbXepHquY+aGEiJbRzi*P#(E@mA`LhaGd)P{RhWM2|QJdXP{VGE$ zd%>`mo4&13h}iTJN@nnQ&dw$cIf5EcE)xM`!RT25u?XB>QnXkDE4`ahu!7P7m%YyX zcZ=QB+qNnk&2Bl#rBZS6iQu7FYy5pncb>4>Zug=Kw~ay1w@?$3c7*8?9D}QPn}@yC z@$9P9Fsn;j9j$%80>!Zv~ycWT^KghrQOp z_rgCMT^fO?qkUBmIPP<2Y-}&l5|+RC@K$MMKR1ggAD4C1?~VByw+pMFJL4}IyW`a5fPeDTnx~8f`z@YFlAdYtq)f75?+CjcWBM^Q~ zBeyokJhAGW3wWhQzR7VsYy#Xt5CAWiLJl4)^UTfkO5P`BbBpy!q*yW<8-O}L+s`wT zjzB6v-ZloK6n$=Db^66yiuaL}JkR;rp%PbES=_(GQsR?Ux_3_p`*)w2tM<&ua=pFd ztM;nwRIOsuVwAxii%6Us_K<`<%34ki((d($h%h>N?N;bx$nZstKzyT-FIHQN9cNoj zDgOEj4~AU*o)Ik(y|WX(NF>hg%57s(}Du>xk=A7xdCwl zx{bwoj~`}8q^YYpfzL&%kl}I#L*xFMP_F%yqV$ka#*sXRW6h`Ub!?(`*A@Gb8;dxu zn}={*TO>Y>Fr6?ejQ|S@S*?^vj=I-@!E62JH^v#5$L!BqLqQb8Etma1VUn8IYIZX4exId3a`*t=Bj7K%q2Kxt`$dUMN7_?jr;mZzt61%Or z^WmH7gg_f*UHBe4IfZODD-k4oPD0 z;JAGv63{+Qs8kl@_rxD6H7z>DhH^G-Zid)Dbo>(RbPW9Wh`R6WcxD&H^ms@LabbV; zIw}`(gjW$EWEmss`5XMn$0y11Z12f*_4KC`l(6&NftW!kmls1~NN2sk#YkzVJYTDh zpb84%9xaT!U|z@*_ij$Q9OUZ2%lem)b>U;U!{MS?7Q0A+iShQyaa&L)PGS}LKryR$g79b`_XvicAj;Fdh-ei=>yqr|8As9pZ-0AOWxa(!{@Qixuv=gG)%s; z(a<2(={A)G!aHBc&Qg$PZD|Z>r@G%M1Yy^0>3ij}cnsp1uwgW#^xgw6A@clZJ)`axzelKv>)M1iyGmjOmDLe&55pn!06Q}KP;tT`-=fIMwvNKun36T zX;a$vt>ad=Vcx!nMFMnoN8*9NW-8sS1@{Coq)@9g@KOC+0uZ1AE z`x05S8m(7`uCne374Oi6QR-c&>yx_)Sli_LIHfaJi%OUGFJNTDUI(156{rubX4oq` zHQqvJ&#~@_OLZ-Ec8Wh1479lo4GeKO3K(&D$dmf?`>&FwgO4kgtnG-$S~k!<&oIoU z(dr-Xa;@=fQ|joY$rDv?r)H0|0b`nu*rs@9M=7x{gV%E1GA~@hf{f2Q>q!pVq`1kl zbl#n{?ozY9n0{1>{z1cP*Fu~jN)v(}9Q337`wyetO}oN9k^l5rD_Y(x#qx&u@$WnB zu@q@&5L@=KqddQ_Bn*Y&eIK*sRx=YLfosIjbW22$09%Xn)@o2^u`Lgysb*3UA)1Z% zt+`e#&aVeG0cFM2G>O+1TfT8<-FJG%$yK~6ajS(nMK?kxd*$s>4lMaD@`!l}tc^Cw z5H{^j>0T8rudIj5jWi*?-a% z7E$s|w9Qg_(}V2J^IL^D{9{s9`a5`_(;pP59P(~;B&pCo@Nmtx!oCs9P9r@NQ;S2f zT1>F))th9DCzAHk@7(kmn{wPqzyQ}0r-`BxRZTI{b=1!9WvbKM+JFFS4A81LjQND9 z@j<;hivq~f_;n3l^(ZEWLho|IrY&L%H{LqRGNL$3Si;9RZ-JVG!?4}_1~K|Ynf_zK zJNX68j7#_MLCjlA0(I7m(a&zbbfMV^jg`mkdh}GzV6iMdNjnZS7=&kjeki6U!MCTj zxA_R^R(EwIm*sh9pIKY}+u<{|Jc4+I71bgtNm$*atzjUE$IC<2n4gVe<%;}0qz1#I z*_(Pl%OJ8nHS!eIYqVO11mmOwb#fdu-W=tp|4G1>*pp&_$xZxVJmnA68a)(#qBVcD>3y?o))l%g$+h8gM zXvO)UeJ;72`A+VY&oftyeSv^w@@RdM7k&^YIJAqR#Z8E3@;N1);7vU9g?I*%9YGLZ z`_#wc9&Y{=oxUhpCq;mFuU_(lr#UFZVe@y{hQYpS9)}=O5qcSXUwPpjH2H6=f;};O zF$Npsy!6_714rKw6$FVKJI{3Md<;0HLMsJTJO!9q3p%8-K{84`z%lBWVzhb~KE)Hh zNkm?rAq`@hANIeMFBNI4?7Yq1pDP5JQ9F7|Oa8fU#{zr=aPr;^Y3^FqfEj(c10`vJ z`?{V4P1*Y!gC!k9i+0=WXM#>e34D=6gg<|U(Fic+Q%8c*;J7KqKi5c^UQr3pr~p-MP4l~*7UKR0pP$mwwoIXzFN zCV-mQ_VroaaM=8Nzic=9+!mQPekL%|iRZU~?pGfR8_pmq#Mi8<%2I-887YR0OR%J3#z*6E0wQmBMZ=?n zBw^89KDb~7$VzXJa_Cm8i1TaHGX1yvjFduDw{=h(v+WQHWj$-T8XAUL5!i!gt#^K?>fW6!oflEgreA)KAqmQZup5c-#wz>AkeyCI5 z-^I1AMoU2>ELKh<+>NuWJ8;Evq_;gcXt>kyB4L>vyL)?o4|+wfg?l}qCehJTb8J{9 zSH$66iU;5%L76aqM>akmTt5_Kp5yC1wH3<)NzK(=H^s~=9f zq023>4p-nuY>`lt=EquVXt{4aVZ{0u@PU?!By;DY>gLC@j`^yop|xGH@Iz)J-0Evw z_0JEmNfJz2O8CGtO7oXgk#L{LfdbFg0h&=^z6My^4MX_W@_+;GiJ(#@hF@lT7!P+g zzP~JFsTwHx1FMI~4-W5)_D<;2?wj~AF#!iVS3=wymP!alqOp1r8J>a#GRct%H-iU6 z)W5_o2{98#Hhy1`9`T47=(WO)>R4z`$1gL@eYi)zNk4dzDPy{`6f*~^^3K1)s3fs@ zev{R6FVI2gH%Zw%_?DgDh9=SYoDBVVAtksJ>pQ4svFY^e44S>JDeGr$O^F+hwzqw$ zaN_inY9%Quj^9BwJLh|j$NAth9GB>l|`)`wJ zAPh3g!4k-o$z;UWao9r1P~?9=JwD<>UhSu}jK03eA!cG)kXvep>QtD&8Jmb{B<#++ z)L3!M^j1TZs%zD+f-}-Dc0I*h`hq-?yax7e-Z%7J{d6g!&e|V9$cTa+XMz#TU7f0r z9}pUcChjm&!hO2&B)1xvutr>)m=;_=w>luXpVCCM2(V1YVX{NmN_P7KIGH6q^Fn{< zdc{+3XUZgCah^aE{4%~$+H|e;X^U+^^`wRp;HbruX=x!cBPB=0j}z`cPA;gx)Cto_M<&ZwJ# zcqi*-k7S`9{{IP7Y*}EH(vRZLrrXh4iNHTijD5J+KTBhI{|6c4VD`6h8rQjoj1JGa zj%)KxqABw9C!c2X7xO)@MelUtmD@$}wB|;{rrzX*3%JNyS2u^@wa?b>IV2(H<|43K z#U)MgMJY+ZK5VyT7_D-^*%4wBUXQ}dDa^g4HBbi><}XPe>Zht&}(t}=%84_t!;i6c##y#L6-Jt9 zyxemeE~mqawx6c>x3hwi|Jjhp_2$FoMq=IjamBCOyV7xGfu*ddEvp+Lm&gEo@+07S zEuBq53w;0KC4HJM9TUp9_;UZ@bx?xSxBO^hROlHq*Pz5$)DMQB84i|m8LVFeH$ele0lc19mY2)1;2>v2glZW&C@F50C`y1@e-tP7$+&d1` zN-{kP6eTanc1^?Ql^aC7rkScO`W;o-ATWXS!0!W>ObFh-{kbCot0E0QxDeH_ zIX$)$d{IBUkqBunE(UZ6oKn+kWahbG7p{8 zpbw!8$s*5&&+X4v_>4Ndfhy}OKoUo&Fc=>1##1?tBo02f)XarQKCb-jIVZ%byII~B z^DNYNue3$#;wLYm=U>s@UOGOK$(+Owuk4=ut%~k6h#mmvZ(y77 zSVC7QT+HduWi%U|{jdtK5#-4A?dhWpPoX?nIqG+7dm$67HMWO@;j}+xV%IfCvM#}0 z_8&4j*dQ8gbnK{;eBb)#TE1rfg(%#D>N`RaQ4^3|M0mGxzL_1)LXz@Y zfgEpdP(PmX3}NMii{yCl0~F(5_#bc$!SZjB1BDdZ2g&9LZ9;L(>2*-fXGCXm>5*4J z2Q*aDl-A`(czdEq#1(6T8*hU(zB54jBCH)m2IO;;Nnw}FZ+e~lR=Kf1mI_X0D|INU zz~*bP5tHzwYiwam6^kBOogJc@Akz&E!=U;l9(dcLq(cnD8|fozm%K%Yf04z%3MPMU zG7v9-OJp1n!*{C_sh8tpe{gfh0R)9e#4C3ff+qJRDq%DZXD~Vw8<=2xf?M zj6dROSC_Xeh17O8HD1J6I-jqE^t%Sx_u};eo*qvD9!^jRrpOzSA7j|r7<%cs-21F}*1ew1 zu*7XYS5+#3PvXuLHLG_9w{%Ka&=(d;4S)aEmA%3wr}3o+r7JhrszcB+sk&DfI3o}x z(2Mr53m4TdBedi=c9OtdGgAb_Z0(opgjr>`AVSLpjH;JNU~ zr4Q`9=5?f=gqoTP{#Wktrk>P*>4dTFNWO-R=XM_)s5Bh!&0Z%K3lADWPZff#eMqRY8b+smEe8nNcLC(z>Lk((n7*B72Y6RXR-${iASD)fzd=i@Ihss9Ja z*73zrZP`TtA$?UnzngOZd06^J4erK;h9e6qGnE z$;tyc6`Jw`HGA6kVmUEW2!v+ZnoA(G6W$n`ZlBxgM7HS)?Y}%l^_pz|@H&?woL7mo zW~^v6L{Kv3sV;tabEAy0gI{z{-h(mJrlbmIP}#g4PrA2pZAhY7NwUl3z^gG*4`5*` z1%6NBjet|pHupS<`gaLVpDl9y{jq?F-^Dhg`ORvHbX*~->xe`jqcr*7nLT8wcN8c8 zJ~;7_M`9-)rukN=oW=r^a*VZaOvjPGI!?{h6)`ArvFzE<)!zhh8c+4F!AZn>w9q}X zH8UJlp}z5ee3^(W+d94_DMU6`7Lp$Mwpa)skLEmOsT5iZ|GXK=ze?D@H8!BD{HC28h8BtTvC3#=@jb+6 zfInY1Gnp{xm^8X95PyB(Ym+j2OMZ;mM}TtW1BtY%LrC~ajl^#*@*K-%>&7Gi(+eUyyE zQhP2DL`7-;w-VH2|K@3~H^mbQ+q_FhYds2dEI8mto-7_3x%x}jT&#avh~!HreY#L( zZU%UX10ZpPC3Wlm&hhf3l`2t=fx;+sMs!&z`sbo=BbA7xiW>p5wv$Y6#dRcl9U)j# z7oxLOH5ay5+#GOGHld#kq;2Z8OtT^g)?zBr2k}|t!Tc87o>ThZs3#RU8{ls% zOY9cPD*hKDK+R8OB?F!??&duKyVn7_V&Rn2U~aj3e(id?$eWx9sSjOWVwzQEaRldo zLsfkoB$BbH(`}X^O3R~l$UU1J+$DN+tN$8FW&UxD3!mYt01#V0&OBnLxDgZrV)-hgkH7bUh~XldrL@RjzIaseY%d6Y~Bsr+^{ zaZ_5xgJC+%x1@@%K`u4je^TZ|CU`Ua+9#+V9hG~T$4`x^rJ*a$v7(6sue- zR90XHg{fI%75`!?;Rzb>CQk^5rl5P;i!|o#J-IN|RMw)o!M0Y;pjee0`R7cKLY>hK zPp$i8PE%Dk;g6w`E&9=RLS5+BW+aJE0UP%Ih;{J7Mb53FX z`oWhIxO6B)i7kLmNwdP6=!+?K)r`5uvH5<0JHC-qL^Uuzgu_+@hlyr{EkPp32~p{9 ziGFcxXpNO%HZThfE_Nn5L~8Ei2$MdJaEMtEVi19TpdW7p(t701(;Bt`X-TTA{MEfG zb5?L*NedcF;fiRN@y7>NP4o{{Uww;a=8k>i=MV&y0ss$H8Hw)!C&=mj z@xhp5;amW2gsiicqs?IXDa|TS z1qPj67+ppWE~Kz~e^?#`Y79_8C+r1bN`DvJ@`kTEbu6NxkUgoWI@}TJX*!GiMfe4S zNgZXrvLI&6ehD=@OB2kWP7+lDQ%y9{)hm?>E#^Cyc#-JHdsuN4&YvIQpSX)N-53^G zW7BvQ-Ze#8rnbrS)YESk#r0U7c6N!j+&tai0klcWhV zCtinZ4~j6){Q29Y=M+^UnszTUV&vsZB=uxA z+vF4qEpM#&GiqIroTeiLU#5mP&j2q^sn$79#7F3x9RMRXu|ioe^a@ARUK+I}!gg># zyZa7Qq**_O8y3-7tdtLCVaN_4uPBGR(;q4}bG(TluPivLGPX2M%=IO5J~4@Nof<48 z0T6=~j%kG2~7abcvm0K+4K|p;5Hn_PH&MKTfuiGUF+9vWNM5 z4?6oh)OlI2hM5;UOfN;kiZc6u56b__I;qL$d+ZCh``kiM0E2FB_v8DTZY3i$ERxfw Pk55ueR