diff options
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/browser-tests | 39 | ||||
-rwxr-xr-x | bin/cobrand-checks | 165 | ||||
-rwxr-xr-x | bin/docker-cobrand | 16 | ||||
-rw-r--r-- | bin/docker.preinit | 45 | ||||
-rwxr-xr-x | bin/fixmystreet.com/backfill-open311-comments | 64 | ||||
-rwxr-xr-x | bin/fixmystreet.com/buckinghamshire-flytipping | 80 | ||||
-rwxr-xr-x | bin/fixmystreet.com/defra_stats | 69 | ||||
-rwxr-xr-x | bin/fixmystreet.com/fixture | 121 | ||||
-rwxr-xr-x | bin/handlemail | 52 | ||||
-rwxr-xr-x | bin/handlemail-support | 16 | ||||
-rwxr-xr-x | bin/install-as-user | 25 | ||||
-rwxr-xr-x | bin/run-tests | 6 | ||||
-rwxr-xr-x | bin/send-comments | 204 | ||||
-rw-r--r-- | bin/site-specific-install.sh | 28 | ||||
-rwxr-xr-x | bin/switch-site | 1 | ||||
-rwxr-xr-x | bin/update-schema | 10 |
16 files changed, 674 insertions, 267 deletions
diff --git a/bin/browser-tests b/bin/browser-tests index 0c56be918..c663e56af 100755 --- a/bin/browser-tests +++ b/bin/browser-tests @@ -12,15 +12,24 @@ BEGIN { use Getopt::Long ':config' => qw(pass_through auto_help); +my ($run_server, $run_cypress, $vagrant); my $config_file = 'conf/general.yml-example'; -my $run_server; -my $run_cypress; -my $vagrant; +my $cobrand = 'fixmystreet'; +my $coords = '51.532851,-2.284277'; +my $area_id = 2608; +my $name = 'Borsetshire'; +my $mapit_url = 'https://mapit.uk/'; + GetOptions( 'config=s' => \$config_file, 'server' => \$run_server, 'cypress' => \$run_cypress, 'vagrant' => \$vagrant, + 'cobrand=s' => \$cobrand, + 'coords=s' => \$coords, + 'area_id=s' => \$area_id, + 'name=s' => \$name, + 'mapit_url=s' => \$mapit_url, ); if ($vagrant) { @@ -62,14 +71,15 @@ sub run { my $config_out = FixMyStreet::TestAppProve->get_config({ config_file => $config_file, # Want this to be like .com - ALLOWED_COBRANDS => [ 'fixmystreet' ], - MAPIT_URL => 'https://mapit.uk/', + ALLOWED_COBRANDS => [ $cobrand ], + MAPIT_URL => $mapit_url, + BASE_URL => 'http://localhost:3001', }); $ENV{FMS_OVERRIDE_CONFIG} = $config_out; # Set up, and load in some data system('bin/make_css', 'fixmystreet.com'); - system('bin/fixmystreet.com/fixture', '--coords', '51.532851,-2.284277', '--name', 'Borsetshire', '--area-id', 2608, '--commit'); + system('bin/fixmystreet.com/fixture', '--nonrandom', '--coords', $coords, '--name', $name, '--area-id', $area_id, '--commit'); } my $pid; @@ -80,11 +90,15 @@ sub run { if (($run_cypress && !$run_server) || $pid) { # Parent, run the test runner (then kill the child) - my $exit = system("cypress", $cmd, '--config', 'fixturesFolder=false,pluginsFile=false,supportFile=false,blacklistHosts=[gaze.mysociety.org,*.openstreetmap.org]', '--project', '.cypress', @ARGV); + my $exit = system("cypress", $cmd, '--config', 'pluginsFile=false,supportFile=false,blacklistHosts=[gaze.mysociety.org,*.openstreetmap.org]', '--project', '.cypress', '--env', "cobrand=$cobrand", @ARGV); kill 'TERM', $pid if $pid; exit $exit >> 8; } else { + use Test::MockModule; + my $c = Test::MockModule->new('FixMyStreet::Cobrand::FixMyStreet'); + $c->mock('enable_category_groups', sub { 1 }); # Child, run the server on port 3001 + FixMyStreet->test_mode(1); # So email doesn't try to send local $ENV{FIXMYSTREET_APP_DEBUG} = 0; require Plack::Runner; my $runner = Plack::Runner->new; @@ -104,15 +118,22 @@ browser-tests - Run Cypress browser tests, set up for FixMyStreet. =head1 SYNOPSIS -browser-tests [options] [cypress options] +browser-tests [running options] [fixture options] [cypress options] - Options: + Running options: --config provide an override general.yml file --server only run the test server, not cypress --cypress only run cypress, not the test server --vagrant run test server inside Vagrant, cypress outside --help this help message + Fixture option: + --cobrand Cobrand to use, default 'fixmystreet' + --coords Default co-ordinates for created reports + --area_id Area ID to use for created body + --name Name to use for created body + --mapit_url MapIt URL to use, default mock + Use browser-tests instead of running cypress directly, so that a clean database is set up for Cypress to use, not affecting your normal dev database. If you're running FixMyStreet in a VM, you can use this script to run the test diff --git a/bin/cobrand-checks b/bin/cobrand-checks new file mode 100755 index 000000000..3673c3b0f --- /dev/null +++ b/bin/cobrand-checks @@ -0,0 +1,165 @@ +#!/bin/bash +# +# cobrand-checks: Check for template changes in a version upgrade + +set -e + +# Read in command line arguments + +POSITIONAL=() +while [[ $# -gt 0 ]] +do +key="$1" +case $key in + -h|--help) + HELP=yes + shift # past argument + ;; + -l|--list) + LIST=yes + shift # past argument + ;; + -d|--diff) + DIFF=yes + shift # past argument + ;; + -i|--interactive) + INTERACTIVE=yes + shift # past argument + ;; + *) # unknown option + POSITIONAL+=("$1") # save it in an array for later + shift # past argument + ;; +esac +done +set -- "${POSITIONAL[@]}" # restore positional parameters + +# Parse command line arguments or work out defaults + +function help { + cat <<END +cobrand-checks COBRAND [OLD-REVISION] [NEW-REVISION] + +If OLD/NEW revisions are not specified, OLD defaults to the most recent tag +accessible from the current checkout, and NEW defaults to either the most +recent tag at all, or HEAD if HEAD is a descendant of the most recent tag at +all. + +With no long argument, acts as if --list is given. + + --list List templates present in COBRAND that have changed in core + between REVISIONs + --diff Show diff of those files + --interactive Step through the files one by one, showing diffs and editing + --help Print usage message and exit +END +} + +if [ "$HELP" ]; then + help + exit 0 +fi + +COBRAND=$1 + +if [ -z "$COBRAND" ]; then + echo "Please provide a cobrand" + echo + help + exit 1 +fi + +if [ -z "$LIST" -a -z "$DIFF" -a -z "$INTERACTIVE" ]; then + LIST=yes +fi + +CURRENT_COMMIT=$(git rev-parse --verify --short --abbrev-ref HEAD) +MOST_RECENT_CHECKED_OUT_TAG=$(git describe --abbrev=0 --tags) +MOST_RECENT_TAG_AT_ALL=$(git describe --tags $(git rev-list --tags --max-count=1)) + +OLD_REV=${2:-$MOST_RECENT_CHECKED_OUT_TAG} +if [[ "$MOST_RECENT_TAG_AT_ALL" == "$MOST_RECENT_CHECKED_OUT_TAG" ]]; then + NEW_REV=${3:-$CURRENT_COMMIT} +else + NEW_REV=${3:-$MOST_RECENT_TAG_AT_ALL} +fi + +# Work out which templates are relevant + +CORE_TEMPLATE_CHANGES=$(git diff --name-only $OLD_REV..$NEW_REV templates/web/base | sort) +COBRAND_TEMPLATES=$(find "templates/web/$COBRAND" -type f \! -name *.ttc | sed "s/$COBRAND/base/" | sort) + +CHANGED_FILES=$(comm -12 <(echo "$CORE_TEMPLATE_CHANGES") <(echo "$COBRAND_TEMPLATES")) + +if [ "$LIST" ]; then + if [ "$CHANGED_FILES" ]; then + echo "$CHANGED_FILES" + fi + exit 0 +fi + +if [ "$DIFF" ]; then + if [ "$CHANGED_FILES" ]; then + git diff --color $OLD_REV..$NEW_REV $CHANGED_FILES + fi + exit 0 +fi + +# --interactive from here + +function title { + printf "\033c" + TITLE="Comparing $OLD_REV to $NEW_REV" + echo $TITLE + printf '=%.0s' $(seq 1 ${#TITLE}) + echo + echo $i + printf '~%.0s' $(seq 1 ${#i}) + echo + echo +} + +function prompt { + cat <<END + +Pick what to do: +0) Compare base $OLD_REV..$NEW_REV +1) Compare $OLD_REV:base to HEAD:$COBRAND +2) Compare $NEW_REV:base to HEAD:$COBRAND +e) Edit $j with vimdiff (:qa to exit) +n) Next +q) Quit +END +} + +( +for i in $CHANGED_FILES; do + title + echo "$(git diff --color $OLD_REV..$NEW_REV $i)" # Avoid pager woes + j=${i/base/$COBRAND} + prompt + while true; do + read -n1 -s -r key + if [ "$key" = '0' ]; then + title + echo "$(git diff --color $OLD_REV..$NEW_REV $i)" + prompt + elif [ "$key" = '1' ]; then + title + diff -u <(git show $OLD_REV:$i) $j || true + prompt + elif [ "$key" = '2' ]; then + title + diff -u <(git show $NEW_REV:$i) $j || true + prompt + elif [ "$key" = 'e' -o "$key" = 'E' ]; then + vim -d <(git show $NEW_REV:$i) $j + elif [ "$key" = 'n' -o "$key" = 'N' ]; then + break; + elif [ "$key" = 'q' -o "$key" = 'Q' ]; then + exit; + fi + done +done +) diff --git a/bin/docker-cobrand b/bin/docker-cobrand new file mode 100755 index 000000000..05c02c1ce --- /dev/null +++ b/bin/docker-cobrand @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +shopt -s nullglob + +LN_FLAGS="-s -f -v" +cobrand="/var/www/fixmystreet/cobrand" +FMS="/var/www/fixmystreet/fixmystreet" + +PATHS=(perllib/FixMyStreet/Cobrand templates/web templates/email web/cobrands conf) + +for path in "${PATHS[@]}"; do + for c in $cobrand/$path/*; do + ln $LN_FLAGS $c $FMS/$path + done +done diff --git a/bin/docker.preinit b/bin/docker.preinit new file mode 100644 index 000000000..459e89de2 --- /dev/null +++ b/bin/docker.preinit @@ -0,0 +1,45 @@ +#!/bin/sh + +# Things to do before starting FixMyStreet in Docker + +# Make sure that the Postgres environment is up and running. +echo "Testing connection to ${FMS_DB_HOST}." +while ! pg_isready -h $FMS_DB_HOST >/dev/null 2>&1 ; do + echo "Still waiting for ${FMS_DB_HOST}..." + sleep 1 +done +echo "Done." + +# If there's a password for the postgres user, set it up for root and see if we need +# to create an FMS user. This is intended for use when using a dedicated local postgres +# container. If this variable doesn't exist, we're going to assume that the FMS user +# has been created already so the stuff below will work. +if [ -n "$POSTGRES_PASSWORD" ]; then + echo "${FMS_DB_HOST}:*:*:postgres:${POSTGRES_PASSWORD}" > /root/.pgpass + chmod 0600 /root/.pgpass + psql -h $FMS_DB_HOST -U postgres postgres -c "create user \"${FMS_DB_USER}\" with CREATEDB password '${FMS_DB_PASS}'" || true +fi + +# Set up a .pgpass for the FMS user. Note that we're assuming the same name for +# both the local shell account and the DB user. +su ${FMS_DB_USER} -c "echo \"${FMS_DB_HOST}:*:*:${FMS_DB_USER}:${FMS_DB_PASS}\" > /home/${FMS_DB_USER}/.pgpass" +chmod 0600 /home/${FMS_DB_USER}/.pgpass + +# If the FMS database doesn't exist, try to create it. +if ! su $FMS_DB_USER -c "psql -h $FMS_DB_HOST -U $FMS_DB_USER -l | egrep \"^ *${FMS_DB_NAME} *\|\" > /dev/null" ; then + su $FMS_DB_USER -c "createdb -h $FMS_DB_HOST -U $FMS_DB_USER --owner \"$FMS_DB_USER\" \"$FMS_DB_NAME\"" +fi + +# Slot in cobrand, if one is present +su $FMS_DB_USER -c "${FMS_ROOT}/bin/docker-cobrand" + +# Ensure things are up to date - schema, CSS, etc +su $FMS_DB_USER -c "${FMS_ROOT}/script/update" + +# Update reports +su $FMS_DB_USER -c "${FMS_ROOT}/bin/update-all-reports" + +# If the right environment variables are present, set up a FMS superuser account. +if [ -n "$SUPERUSER_PASSWORD" ] && [ -n "$SUPERUSER_EMAIL" ]; then + su $FMS_DB_USER -c "${FMS_ROOT}/bin/createsuperuser $SUPERUSER_EMAIL $SUPERUSER_PASSWORD" +fi diff --git a/bin/fixmystreet.com/backfill-open311-comments b/bin/fixmystreet.com/backfill-open311-comments new file mode 100755 index 000000000..a40ed3a7e --- /dev/null +++ b/bin/fixmystreet.com/backfill-open311-comments @@ -0,0 +1,64 @@ +#!/usr/bin/env perl +# +# This script utilises the Open311 extension explained at +# https://github.com/mysociety/FixMyStreet/wiki/Open311-FMS---Proposed-differences-to-Open311 +# to fetch updates on service requests for a specified timeframe, optionally for a single body + +use strict; +use warnings; +require 5.8.0; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use DateTime; +use DateTime::Format::W3CDTF; +use DateTime::Format::ISO8601; +use Getopt::Long::Descriptive; + +my ($opt, $usage) = describe_options( + '%c %o', + [ 'body:s', "Name of body to use" ], + [ 'start_date:s', "Date of first comment to fetch" ], + [ 'end_date:s', "Date of last comment to fetch" ], + [ 'verbose', "Print out info as we go" ], + [ 'help', "print usage message and exit", { shortcircuit => 1 } ], +); +print($usage->text), exit if $opt->help; + +use Open311::GetServiceRequestUpdates; + +my $start_date = DateTime::Format::W3CDTF->parse_datetime( $opt->start_date ); +my $end_date = DateTime::Format::W3CDTF->parse_datetime( $opt->end_date ); + +die "start and end date required\n" unless $start_date and $end_date; + +die "start date must be before end date\n" unless $end_date > $start_date; + +my $current_date = $start_date->clone; + +while ( $current_date < $end_date ) { + my $current_plus_24 = $current_date->clone; + $current_plus_24->add( hours => 24 ); + + print "fetching updates from $current_date till $current_plus_24\n" if $opt->verbose; + + my %params = ( + verbose => $opt->verbose, + start_date => DateTime::Format::W3CDTF->format_datetime( $current_date ), + end_date => DateTime::Format::W3CDTF->format_datetime( $current_plus_24 ) + ); + + $params{body} = $opt->body if $opt->body; + + my $updates = Open311::GetServiceRequestUpdates->new( %params ); + $updates->fetch; + + $current_date = $current_plus_24; + + sleep 5; +} diff --git a/bin/fixmystreet.com/buckinghamshire-flytipping b/bin/fixmystreet.com/buckinghamshire-flytipping new file mode 100755 index 000000000..27548be88 --- /dev/null +++ b/bin/fixmystreet.com/buckinghamshire-flytipping @@ -0,0 +1,80 @@ +#!/usr/bin/env perl +# +# If a district flytipping report within Buckinghamshire has not been closed +# after three weeks, close it with a message. If it's older than six weeks, +# use a different message and suppress any alerts. + +use v5.14; +use warnings; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use constant BUCKS_AREA_ID => 2217; +use constant DISTRICT_IDS => (2255, 2256, 2257, 2258); +use constant TIME_OPEN => '3 weeks'; +use constant TIME_OPEN_ALERT => '6 weeks'; + +use FixMyStreet::DB; +use FixMyStreet::Script::ArchiveOldEnquiries; +use Getopt::Long::Descriptive; + +my ($opts, $usage) = describe_options( + '%c %o', + ['commit|c', "actually close reports and send emails. Omitting this flag will do a dry-run"], + ['help|h', "print usage message and exit" ], +); +print($usage->text), exit if $opts->help; + +my $body = FixMyStreet::DB->resultset("Body")->for_areas(BUCKS_AREA_ID)->first; +die "Could not find Bucks body" unless $body; + +my @districts = FixMyStreet::DB->resultset("Body")->for_areas(DISTRICT_IDS)->all; +my @district_ids = map { $_->id } @districts; +die "Did not find all districts" unless @district_ids == 4; + +find_problems(TIME_OPEN_ALERT, TIME_OPEN, 'Auto-closure', 1); +find_problems(undef, TIME_OPEN_ALERT, 'Auto-closure (old)', 0); + +sub find_problems { + my ($from, $to, $title, $retain_alerts) = @_; + + my $template = FixMyStreet::DB->resultset("ResponseTemplate")->search({ + body_id => $body->id, title => $title, + })->first; + die "Could not find Bucks Flytipping template" unless $template; + + $to = "current_timestamp - '$to'::interval"; + my $time_param; + if ($from) { + $from = "current_timestamp - '$from'::interval"; + $time_param = [ -and => { '>=', \$from }, { '<', \$to } ], + } else { + $time_param = { '<', \$to }; + } + + # Fetch all Flytipping problems sent only to districts, between $from and $to + my $q = FixMyStreet::DB->resultset("Problem")->search( + \[ "? @> regexp_split_to_array(bodies_str, ',')", [ {} => \@district_ids ] ] + )->search({ + state => [ FixMyStreet::DB::Result::Problem->open_states() ], + category => 'Flytipping', + confirmed => $time_param, + }); + + # Provide some variables to the archiving script + FixMyStreet::Script::ArchiveOldEnquiries::update_options({ + user => $body->comment_user->id, + user_name => $body->comment_user->name, + closure_text => $template->text, + retain_alerts => $retain_alerts, + commit => $opts->commit, + }); + + # Close the reports + FixMyStreet::Script::ArchiveOldEnquiries::close_problems($q); +} diff --git a/bin/fixmystreet.com/defra_stats b/bin/fixmystreet.com/defra_stats new file mode 100755 index 000000000..563559a21 --- /dev/null +++ b/bin/fixmystreet.com/defra_stats @@ -0,0 +1,69 @@ +#!/usr/bin/env perl +# +# this script generates litter stats for defra + +use strict; +use warnings; +use v5.14; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use FixMyStreet; +use FixMyStreet::DB; +use Utils; +use Text::CSV; + +# list of categories pulled from FMS Aug 2018 +my @categories = ( + 'Accumulated Litter', 'Damage Public Litter Bin', 'Dog and litter bins', 'Dog Bin', + 'Dog Bin overflow', 'Dog bin overflowing', 'Dog fouling', 'Dog Fouling', 'Dog Mess', + 'Dogs and Dogs Fouling', 'Dog Waste Bin', 'Dog Waste Bin on a verge or open space', + 'Dumped cylinder', 'Dumped rubbish', 'Flytipping', 'Fly tipping', 'Fly-tipping', + 'Fly Tipping', 'Fly-Tipping', 'Flytipping and dumped rubbish', + 'Fly Tipping on a public right of way', 'Fly Tipping on a road, footway, verge or open space', + 'Litter', 'Litter bin', 'Litter Bin', 'Litter Bin on a verge or open space', + 'Litter Bin overflow', 'Litter Bin Overflowing', 'Litter in the street', + 'Overflowing litter bin', 'Overflowing Street Litter Bin', 'Rubbish or fly tipping on the roads', +); + +use Getopt::Long::Descriptive; + +my ($opt, $usage) = describe_options( + '%c %o', + [ 'file=s', "Filename to output results to", { required => 1 } ], + [ 'start_date=s', "Start date for stats, defaults to 6 months ago" ], + [ 'end_date=s', "End date for stats, defaults today" ], + [ 'help', "print usage message and exit", { shortcircuit => 1 } ], +); +print($usage->text), exit if $opt->help; + +my $now = DateTime->now(); +my $start_date = $opt->start_date || $now->clone->add(months => -6)->strftime('%Y-%m-%d'); +my $end_date = $opt->end_date || $now->clone->strftime('%Y-%m-%d'); + +my $fh; +open($fh, '>', $opt->file) or die $!; + +my @rows; +my $csv = Text::CSV->new({ eol => $/ }); +$csv->print( $fh, [ qw(Latitude Longitude Easting Northing Date Category) ] ); + +my $problems = FixMyStreet::DB->resultset('Problem')->search( { + category => \@categories, + confirmed => { '>=' => $start_date, '<=' => $end_date } + } +); + +while (my $p = $problems->next) { + my ( $easting, $northing ) = Utils::convert_latlon_to_en( + $p->latitude, + $p->longitude + ); + + $csv->print($fh, [ $p->latitude, $p->longitude, $easting, $northing, $p->confirmed, $p->category ] ); +} diff --git a/bin/fixmystreet.com/fixture b/bin/fixmystreet.com/fixture index 091fcab9d..622bae018 100755 --- a/bin/fixmystreet.com/fixture +++ b/bin/fixmystreet.com/fixture @@ -19,6 +19,7 @@ use List::Util qw(shuffle); use Path::Tiny; use FixMyStreet; use FixMyStreet::Cobrand; +use FixMyStreet::PhotoStorage; use FixMyStreet::DB::Factories; use Getopt::Long::Descriptive; @@ -28,6 +29,7 @@ my ($opt, $usage) = describe_options( [ 'name:s', "Name of body to use (defaults to MapIt area name)" ], [ 'empty', "Empty all tables of the database first" ], [ 'commit', "Actually commit changes to the database" ], + [ 'nonrandom', "Output things in a fixed manner, for testing" ], [ 'coords=s', "Co-ordinates to use instead of example postcode" ], [ 'help', "print usage message and exit", { shortcircuit => 1 } ], ); @@ -36,7 +38,28 @@ print($usage->text), exit if $opt->help; FixMyStreet::DB::Factories->setup($opt); # Body + categories -my $categories = ['Potholes', 'Street lighting', 'Graffiti', 'Other']; +my $categories = [ + 'Abandoned vehicles', + 'Bus stops', + 'Dog fouling', + 'Flyposting', + 'Flytipping', + 'Footpath/bridleway away from road', + 'Graffiti', + 'Parks/landscapes', + 'Pavements', + 'Potholes', + 'Public toilets', + 'Roads/highways', + 'Road traffic signs', + 'Rubbish (refuse and recycling)', + 'Street cleaning', + 'Street lighting', + 'Street nameplates', + 'Traffic lights', + 'Trees', + 'Other', +]; my $body = FixMyStreet::DB::Factory::Body->find_or_create({ area_id => $opt->area_id, categories => $categories, @@ -44,6 +67,28 @@ my $body = FixMyStreet::DB::Factory::Body->find_or_create({ }); say "Created body " . $body->name . " for MapIt area ID " . $opt->area_id . ', categories ' . join(', ', @$categories); +for my $cat (qw/Overflowing Broken Missing/) { + my $child_cat = FixMyStreet::DB::Factory::Contact->find_or_create({ + body => $body, + category => $cat + }); + $child_cat->set_extra_metadata( group => 'Bins' ); + $child_cat->update; +} + +for my $cat ('Dropped Kerbs', 'Skips') { + my $child_cat = FixMyStreet::DB::Factory::Contact->find_or_create({ + body => $body, + category => $cat + }); + $child_cat->set_extra_metadata( group => 'Licensing' ); + $child_cat->set_extra_fields( + { description => 'Start date', code => 'start_date', datatype => 'string', fieldtype => 'date' }, + { description => 'End date', code => 'end_date', datatype => 'string', fieldtype => 'date' } + ); + $child_cat->update; +} + FixMyStreet::DB::Factory::ResponseTemplate->create({ body => $body, title => 'Generic', text => 'Thank you for your report, we will be in touch with an update soon.' }); @@ -73,9 +118,15 @@ my $perms_cs = [ 'contribute_as_body', 'contribute_as_another_user', 'moderate', 'view_body_contribute_details', ]; +my $perms_cs_full = [ + 'contribute_as_body', 'contribute_as_another_user', + 'moderate', 'view_body_contribute_details', + 'report_prefill', 'default_to_body' +]; foreach ( { name => 'Inspector Gadget', email => 'inspector@example.org', email_verified => 1, body => $body, permissions => $perms_inspector }, { name => 'Harriet Helpful', email_verified => 1, email => 'cs@example.org', body => $body, permissions => $perms_cs }, + { name => 'Andrew Agreeable', email_verified => 1, email => 'cs_full@example.org', body => $body, permissions => $perms_cs_full }, { name => 'Super User', email_verified => 1, email => 'super@example.org', body => $body, permissions => [ @$perms_cs, @$perms_inspector, 'report_edit', 'category_edit', 'template_edit', 'responsepriority_edit', @@ -111,17 +162,47 @@ foreach (FixMyStreet::Cobrand->available_cobrand_classes) { } } -my $cache_dir = path(FixMyStreet->config('UPLOAD_DIR')); -$cache_dir->mkpath; +FixMyStreet::PhotoStorage::backend->init(); my $user = $users{'user@example.org'}; -my $num = 20; +my $num = $opt->nonrandom ? 21 : 50; say "Created $num problems around '$location' in cobrand '$cobrand'"; my $confirmed = DateTime->today->subtract(days => 1)->add(hours => 8); + +my @problem_data; +if ($opt->nonrandom) { + my $data = FixMyStreet::DB::Factory::Problem->data; + my @config = ( + { category => 'Potholes', count => 6, times => [ 1000, 2000, 3000 ] }, + { category => 'Street lighting', count => 5, times => [ 750, 2100, 2900, 4000 ] }, + { category => 'Graffiti', count => 5, times => [ 1501, 1500, 500 ] }, + { category => 'Other', count => 5, times => [ 6000, 14000 ] }, + ); + for my $c (@config) { + my $category = $c->{category}; + for (my $i = 0; $i < $c->{count}; $i++) { + my $time = $confirmed->clone->add(seconds => $c->{times}->[$i] || ( rand(7000) + 6000 )); + push @problem_data, { + title => $data->{titles}->{$category}->[$i], + detail => $data->{descriptions}->{$category}->[$i], + category => $category, + confirmed => $time, + }; + } + } +} else { + for (1..$num) { + $confirmed->add(seconds => rand(7000)); + my $category = $categories->[int(rand(@$categories))]; + push @problem_data, { + category => $category, + confirmed => $confirmed, + }; + } +} + my $problems = []; -for (1..$num) { - $confirmed->add(seconds => rand(7000)); - my $category = $categories->[int(rand(@$categories))]; +for (0..$num-1) { push @$problems, FixMyStreet::DB::Factory::Problem->create_problem({ body => $body, areas => ',' . $opt->area_id . ',', @@ -129,13 +210,13 @@ for (1..$num) { postcode => $location, latitude => $lat, longitude => $lon, - category => $category, cobrand => $cobrand, - confirmed => $confirmed, + lastupdate => $problem_data[$_]->{confirmed}, + %{$problem_data[$_]}, }); } -for (1..3) { +for (1..5) { my $p = $problems->[int(rand(@$problems))]; $users{'inspector@example.org'}->add_to_planned_reports($p); } @@ -152,9 +233,10 @@ my @fixed_user = ( 'Bish bash bosh. Sorted. Thanks so much.', ); -my @problems = shuffle(@$problems); +my @problems = $opt->nonrandom ? @$problems : shuffle(@$problems); -for (1..3) { +my @range = $opt->nonrandom ? (1, 7, 12) : (1..10); +for (@range) { my $problem = $problems[$_]; $confirmed->add(seconds => rand(10000)); FixMyStreet::DB::Factory::Comment->create({ @@ -202,6 +284,7 @@ for (1..3) { text => $fixed_user[int(rand(@fixed_user))], confirmed => DateTime::Format::Pg->format_datetime($confirmed), }); + $problem->update( { lastupdate => DateTime::Format::Pg->format_datetime($confirmed) } ); } # Some 'still open' updates @@ -210,22 +293,26 @@ my @open_user = ( 'Ongoing issue.', 'Council rang to say they’re aware and it’s on their list.', 'Still awaiting news on this one.', - 'Council let me know it’s not a top priority, which TBH I do understand now they’ve talked it through.', + 'Council let me know it’s not a top priority, which TBH I do understand now they’ve talked it through.', ); my $updates = []; -for (5..9) { +@range = $opt->nonrandom ? (13, 8, 2) : (11..20); +for my $i (@range) { $confirmed->add(seconds => rand(10000)); + my @range_u = $opt->nonrandom ? (1..$i) : (1); push @$updates, FixMyStreet::DB::Factory::Comment->create({ - problem => $problems[$_], + problem => $problems[$i], user => $user, text => $open_user[int(rand(@open_user))], confirmed => DateTime::Format::Pg->format_datetime($confirmed), - }); + }) for (@range_u); + $problems[$i]->update( { lastupdate => DateTime::Format::Pg->format_datetime($confirmed) } ); } # Some not responsible updates -for (11..13) { +@range = $opt->nonrandom ? (3, 9, 20) : (21..25); +for (@range) { my $problem = $problems[$_]; $confirmed->add(seconds => rand(10000)); push @$updates, FixMyStreet::DB::Factory::Comment->create({ diff --git a/bin/handlemail b/bin/handlemail index f85ad3e65..ade5d42d3 100755 --- a/bin/handlemail +++ b/bin/handlemail @@ -19,10 +19,11 @@ BEGIN { require "$d/../setenv.pl"; } +use Getopt::Long; +use Path::Tiny; use FixMyStreet; use FixMyStreet::DB; use FixMyStreet::Email; -use mySociety::EmailUtil; use mySociety::HandleMail; use mySociety::SystemMisc qw(print_log); @@ -30,6 +31,15 @@ use mySociety::SystemMisc qw(print_log); # messages being generated (only in response to non-bounce input, obviously). mySociety::SystemMisc::log_to_stderr(0); +my $cobrand = "default"; + +# Where to forward mail that should be looked at by a person, +# such as a permanent bounce for a report. +my $bouncemgr = FixMyStreet->config('CONTACT_EMAIL'); + +GetOptions ("cobrand=s" => \$cobrand, + "bouncemgr=s" => \$bouncemgr); + my %data = mySociety::HandleMail::get_message(); my @lines = @{$data{lines}}; my $token = get_envelope_token(); @@ -104,15 +114,23 @@ sub handle_permanent_bounce { $object->disable(); } elsif ($type eq 'report') { print_log('info', "Received bounce for report " . $object->id . ", forwarding to support"); - forward_on_to(FixMyStreet->config('CONTACT_EMAIL')); + forward_on_to($bouncemgr); } } sub is_out_of_office { my (%attributes) = @_; return 1 if $attributes{problem} && $attributes{problem} == mySociety::HandleMail::ERR_OUT_OF_OFFICE; - my $subject = $data{message}->head()->get("Subject"); - return 1 if $subject =~ /Auto(matic|mated)?[ -_]?(reply|response|responder)|Thank[ _]you[ _]for[ _](your[ _]email|contacting)|Out of (the )?Office|away from the office|This office is closed until|^Re: (Problem Report|New updates)|^Auto: |^E-Mail Response$|^Message Received:|have received your email|Acknowledgement of your email|away from my desk/i; + my $head = $data{message}->head(); + return 1 if $head->get('X-Autoreply') || $head->get('X-Autorespond'); + my $mc = $head->get('X-POST-MessageClass') || ''; + return 1 if $mc eq '9; Autoresponder'; + my $auto_submitted = $head->get("Auto-Submitted") || ''; + return 1 if $auto_submitted && $auto_submitted !~ /no/; + my $precedence = $head->get("Precedence") || ''; + return 1 if $precedence =~ /auto_reply/; + my $subject = $head->get("Subject"); + return 1 if $subject =~ /Auto(matic|mated)?[ -_]?(reply|response|responder)|Thank[ _]you[ _]for[ _](your[ _]email|contacting)|Out of (the )?Office|away from the office|This office is closed until|^Re: (Problem Report|New updates)|^Auto: |^E-Mail Response$|^Message Received:|have received your email|Acknowledgement of your email|away from my desk|We got your email/i; return 0; } @@ -136,7 +154,7 @@ sub handle_bounce_to_verp_address { handle_non_bounce_to_verp_address(); } elsif (!$info) { print_log('info', "Unparsed bounce received for $type " . $object->id . ", forwarding to support"); - forward_on_to(FixMyStreet->config('CONTACT_EMAIL')); + forward_on_to($bouncemgr); } else { print_log('info', "Ignoring bounce received for $type " . $object->id . $info); } @@ -145,7 +163,7 @@ sub handle_bounce_to_verp_address { sub handle_non_bounce_to_verp_address { if ($type eq 'alert' && !is_out_of_office()) { print_log('info', "Received non-bounce for alert " . $object->id . ", forwarding to support"); - forward_on_to(FixMyStreet->config('CONTACT_EMAIL')); + forward_on_to($bouncemgr); } elsif ($type eq 'report') { print_log('info', "Received non-bounce for report " . $object->id . ", forwarding to report creator"); forward_on_to($object->user->email); @@ -161,18 +179,19 @@ sub handle_non_bounce_to_null_address { # Send an automatic response print_log('info', "Received non-bounce to null address, auto-replying"); - my $template = 'reply-autoresponse'; - my $fp = FixMyStreet->path_to("templates", "email", "default", $template)->open or exit 75; - $template = join('', <$fp>); - $fp->close; + + my $template = path(FixMyStreet->path_to("templates", "email", $cobrand, 'reply-autoresponse'))->slurp_utf8; # We generate this as a bounce. + my ($rp) = $data{return_path} =~ /^\s*<(.*)>\s*$/; my $mail = FixMyStreet::Email::construct_email({ - From => [ FixMyStreet->config('CONTACT_EMAIL'), 'FixMyStreet' ], - To => $data{return_path}, + 'Auto-Submitted' => 'auto-replied', + From => [ FixMyStreet->config('CONTACT_EMAIL'), + FixMyStreet->config('CONTACT_NAME') ], + To => $rp, _body_ => $template, }); - send_mail($mail->as_string, $data{return_path}); + send_mail($mail, $rp); } sub forward_on_to { @@ -182,9 +201,10 @@ sub forward_on_to { } sub send_mail { - my ($text, $recipient) = @_; - if (mySociety::EmailUtil::EMAIL_SUCCESS - != mySociety::EmailUtil::send_email($text, '<>', $recipient)) { + my ($email, $recipient) = @_; + unless (FixMyStreet::Email::Sender->try_to_send( + $email, { from => '<>', to => $recipient } + )) { exit(75); } } diff --git a/bin/handlemail-support b/bin/handlemail-support index 0ccde8ca7..975773bd9 100755 --- a/bin/handlemail-support +++ b/bin/handlemail-support @@ -22,7 +22,7 @@ BEGIN { } use FixMyStreet; -use mySociety::EmailUtil; +use FixMyStreet::Email::Sender; use mySociety::HandleMail; my %data = mySociety::HandleMail::get_message(); @@ -33,12 +33,14 @@ forward_on(); sub forward_on { my ($l, $d) = split /\@/, FixMyStreet->config('CONTACT_EMAIL'); - if (mySociety::EmailUtil::EMAIL_SUCCESS - != mySociety::EmailUtil::send_email( - join("\n", @{$data{lines}}) . "\n", - $data{return_path}, - join('@', join('_deli', $l, 'very'), $d) - )) { + my ($rp) = $data{return_path} =~ /^\s*<(.*)>\s*$/; + unless (FixMyStreet::Email::Sender->try_to_send( + join("\n", @{$data{lines}}) . "\n", + { + from => $rp, + to => join('@', join('_deli', $l, 'very'), $d) + } + )) { exit 75; } exit 0; diff --git a/bin/install-as-user b/bin/install-as-user index e42401758..ce6facbd1 100755 --- a/bin/install-as-user +++ b/bin/install-as-user @@ -25,6 +25,8 @@ misuse() { } [ -z "$DEVELOPMENT_INSTALL" ] && misuse DEVELOPMENT_INSTALL +[ -z "$DOCKER" ] && misuse DOCKER +[ -z "$INSTALL_DB" ] && misuse INSTALL_DB DB_NAME="fixmystreet" @@ -92,19 +94,20 @@ then fi echo $DONE_MSG -# Create the database if it doesn't exist: -echo -n "Setting up database... " -if ! psql -l | egrep "^ *$DB_NAME *\|" > /dev/null -then - createdb --owner "$UNIX_USER" "$DB_NAME" - echo 'CREATE LANGUAGE plpgsql;' | psql -U "$UNIX_USER" "$DB_NAME" || true +if [ $INSTALL_DB = true ]; then + # Create the database if it doesn't exist: + echo -n "Setting up database... " + if ! psql -l | egrep "^ *$DB_NAME *\|" > /dev/null + then + createdb --owner "$UNIX_USER" "$DB_NAME" + echo 'CREATE LANGUAGE plpgsql;' | psql -U "$UNIX_USER" "$DB_NAME" || true + fi + bin/update-schema --commit + bin/update-all-reports + echo $DONE_MSG fi -bin/update-schema --commit -echo $DONE_MSG # Generate po and mo files (these invocations taken from Kagee's script): echo "Creating locale .mo files" -commonlib/bin/gettext-makemo FixMyStreet +commonlib/bin/gettext-makemo FixMyStreet 2>&1 echo $DONE_MSG - -bin/update-all-reports diff --git a/bin/run-tests b/bin/run-tests index c384516c8..de495dd05 100755 --- a/bin/run-tests +++ b/bin/run-tests @@ -1,13 +1,17 @@ #!/usr/bin/env perl use strict; use warnings; +my $d; BEGIN { use File::Basename qw(dirname); use File::Spec; - my $d = dirname(File::Spec->rel2abs($0)); + $d = dirname(File::Spec->rel2abs($0)); require "$d/../setenv.pl"; } +# So the t::Mock:: modules can be found +$ENV{PERL5LIB} = "$d/..:$ENV{PERL5LIB}"; + use FixMyStreet::TestAppProve; =head1 NAME diff --git a/bin/send-comments b/bin/send-comments index fc61169ef..665f377bc 100755 --- a/bin/send-comments +++ b/bin/send-comments @@ -18,206 +18,12 @@ BEGIN { } use CronFns; - -use DateTime; use FixMyStreet; -use FixMyStreet::Cobrand; -use FixMyStreet::DB; -use FixMyStreet::Email; -use FixMyStreet::Map; -use Open311; - -# send_method config values found in by-area config data, for selecting to appropriate method -use constant SEND_METHOD_EMAIL => 'email'; -use constant SEND_METHOD_OPEN311 => 'Open311'; - -use constant COUNCIL_ID_OXFORDSHIRE => 2237; -use constant COUNCIL_ID_BROMLEY => 2482; -use constant COUNCIL_ID_LEWISHAM => 2492; -use constant COUNCIL_ID_BANES => 2551; +use Open311::PostServiceRequestUpdates; -# Set up site, language etc. my ($verbose, $nomail) = CronFns::options(); -my $base_url = FixMyStreet->config('BASE_URL'); -my $site = ''; -$site = 'fixmystreet.com' if $base_url eq "https://www.fixmystreet.com"; - -my $bodies = FixMyStreet::DB->resultset('Body')->search( { - send_method => SEND_METHOD_OPEN311, - send_comments => 1, -} ); - -while ( my $body = $bodies->next ) { - - # XXX Cobrand specific - see also list in Problem->updates_sent_to_body - if ($site eq 'fixmystreet.com') { - # Oxfordshire (OCC) is special: - # we do *receive* service_request_updates (aka comments) for OCC, but we never *send* them, so skip this pass - next if $body->areas->{+COUNCIL_ID_OXFORDSHIRE}; - # Lewisham does not yet accept updates - next if $body->areas->{+COUNCIL_ID_LEWISHAM}; - } - - my $use_extended = 0; - my $comments = FixMyStreet::DB->resultset('Comment')->search( { - 'me.whensent' => undef, - 'me.external_id' => undef, - 'me.state' => 'confirmed', - 'me.confirmed' => { '!=' => undef }, - 'problem.whensent' => { '!=' => undef }, - 'problem.external_id' => { '!=' => undef }, - 'problem.bodies_str' => { -like => '%' . $body->id . '%' }, - 'problem.send_method_used' => 'Open311', - }, - { - join => 'problem', - order_by => [ 'confirmed', 'id' ], - } - ); - - if ( $site eq 'fixmystreet.com' && $body->areas->{+COUNCIL_ID_BROMLEY} ) { - $use_extended = 1; - } - - my %open311_conf = ( - endpoint => $body->endpoint, - jurisdiction => $body->jurisdiction, - api_key => $body->api_key, - use_extended_updates => $use_extended, - ); - - - if ( $body->send_extended_statuses ) { - $open311_conf{extended_statuses} = 1; - } - - my $o = Open311->new( %open311_conf ); - - if ( $site eq 'fixmystreet.com' && $body->areas->{+COUNCIL_ID_BROMLEY} ) { - my $endpoints = $o->endpoints; - $endpoints->{update} = 'update.xml'; - $endpoints->{service_request_updates} = 'update.xml'; - $o->endpoints( $endpoints ); - } - - while ( my $comment = $comments->next ) { - my $cobrand = $body->get_cobrand_handler || - FixMyStreet::Cobrand->get_class_for_moniker($comment->cobrand)->new(); - - # Some cobrands (e.g. Buckinghamshire) don't want to receive updates - # from anyone except the original problem reporter. - if ($cobrand->call_hook(should_skip_sending_update => $comment)) { - unless (defined $comment->get_extra_metadata('cobrand_skipped_sending')) { - $comment->set_extra_metadata(cobrand_skipped_sending => 1); - $comment->update; - } - next; - } - - # TODO actually this should be OK for any devolved endpoint if original Open311->can_be_devolved, presumably - if ( 0 ) { # Check can_be_devolved and do this properly if set - my $sender = $cobrand->get_body_sender( $body, $comment->problem->category ); - my $config = $sender->{config}; - $o = Open311->new( - endpoint => $config->endpoint, - jurisdiction => $config->jurisdiction, - api_key => $config->api_key, - use_extended_updates => 1, # FMB uses extended updates - ); - } - - next if !$verbose && $comment->send_fail_count && retry_timeout($comment); - if ( $site eq 'fixmystreet.com' && $body->areas->{+COUNCIL_ID_BROMLEY} ) { - my $extra = $comment->extra; - if ( !$extra ) { - $extra = {}; - } - - unless ( $extra->{title} ) { - $extra->{title} = $comment->user->title; - $comment->extra( $extra ); - } - } - - my $id = $o->post_service_request_update( $comment ); - - if ( $id ) { - send_comment_email($comment, $cobrand) if $body->areas->{+COUNCIL_ID_BANES}; - $comment->update( { - external_id => $id, - whensent => \'current_timestamp', - } ); - } else { - $comment->update( { - send_fail_count => $comment->send_fail_count + 1, - send_fail_timestamp => \'current_timestamp', - send_fail_reason => "Failed to post over Open311\n\n" . $o->error, - } ); - - if ( $verbose && $o->error ) { - warn $o->error; - } - } - } -} - -sub retry_timeout { - my $row = shift; - - my $tz = FixMyStreet->local_time_zone; - my $now = DateTime->now( time_zone => $tz ); - my $diff = $now - $row->send_fail_timestamp; - if ( $diff->in_units( 'minutes' ) < 30 ) { - return 1; - } - - return 0; -} - -=head2 send_comment_email - -Some cobrands (e.g. BANES) want to receive an email for every update that's sent -via Open311. This function is called after each update is sent, and sends the -alert-update.txt templated email to the cobrand's update_email (or -contact_email if update_email isn't defined.) - -=cut -sub send_comment_email { - my ($comment, $cobrand) = @_; - - my $handler = $cobrand->call_hook(get_body_handler_for_problem => $comment->problem) or return; - - # Set up map/language so things don't error - FixMyStreet::Map::set_map_class($handler->map_type); - $handler->set_lang_and_domain( $comment->lang, 1, FixMyStreet->path_to('locale')->stringify ); - my $to = $cobrand->call_hook('update_email') || $cobrand->contact_email; - - # Construct the data the alert-update email template needs - # (bit annoying that we can't just put $comment in data!) - my %data = ( - cobrand => $handler, - hide_unsubscribe => 1, - data => [ { - item_photo => $comment->photo, - item_text => $comment->text, - item_name => $comment->name, - item_anonymous => $comment->anonymous, - confirmed => $comment->confirmed, - get_first_image_fp => sub { $comment->get_first_image_fp }, - } ], - report => $comment->problem, - problem_url => $handler->base_url_for_report($comment->problem) . $comment->problem->url, - ); - - FixMyStreet::Email::send_cron( - FixMyStreet::DB->schema, - "alert-update.txt", - \%data, - { To => $to }, - undef, - 0, - $handler, - $comment->lang, - ); -} +my $updates = Open311::PostServiceRequestUpdates->new( + verbose => $verbose, +); +$updates->send; diff --git a/bin/site-specific-install.sh b/bin/site-specific-install.sh index 637572a94..f0b78302f 100644 --- a/bin/site-specific-install.sh +++ b/bin/site-specific-install.sh @@ -1,7 +1,7 @@ #!/bin/sh # Set this to the version we want to check out -VERSION=${VERSION_OVERRIDE:-v2.3.4} +VERSION=${VERSION_OVERRIDE:-v2.6} PARENT_SCRIPT_URL=https://github.com/mysociety/commonlib/blob/master/bin/install-site.sh @@ -26,27 +26,41 @@ misuse() { [ -z "$DISTRIBUTION" ] && misuse DISTRIBUTION [ -z "$VERSION" ] && misuse VERSION [ -z "$DEVELOPMENT_INSTALL" ] && misuse DEVELOPMENT_INSTALL +[ -z "$DOCKER" ] && misuse DOCKER +[ -z "$INSTALL_DB" ] && misuse INSTALL_DB +[ -z "$INSTALL_POSTFIX" ] && misuse INSTALL_POSTFIX add_locale cy_GB add_locale nb_NO add_locale de_CH -install_postfix +if [ $INSTALL_POSTFIX = true ]; then + install_postfix +fi if [ ! "$DEVELOPMENT_INSTALL" = true ]; then - install_nginx - add_website_to_nginx + if [ ! "$DOCKER" = true ]; then + install_nginx + add_website_to_nginx + fi # Check out the current released version su -l -c "cd '$REPOSITORY' && git checkout '$VERSION' && git submodule update" "$UNIX_USER" fi +# Create a log directoryfor Docker builds - this is normally done above. +if [ $DOCKER = true ]; then + make_log_directory +fi + install_website_packages su -l -c "touch '$DIRECTORY/admin-htpasswd'" "$UNIX_USER" -add_postgresql_user +if [ $INSTALL_DB = true ]; then + add_postgresql_user +fi -export DEVELOPMENT_INSTALL +export DEVELOPMENT_INSTALL DOCKER INSTALL_DB su -c "$REPOSITORY/bin/install-as-user '$UNIX_USER' '$HOST' '$DIRECTORY'" "$UNIX_USER" if [ ! "$DEVELOPMENT_INSTALL" = true ]; then @@ -61,7 +75,7 @@ then overwrite_rc_local fi -if [ ! "$DEVELOPMENT_INSTALL" = true ]; then +if [ ! "$DEVELOPMENT_INSTALL" = true ] && [ ! "$DOCKER" = true ]; then # Tell the user what to do next: echo Installation complete - you should now be able to view the site at: diff --git a/bin/switch-site b/bin/switch-site index c4ca53ca9..bd4be5090 100755 --- a/bin/switch-site +++ b/bin/switch-site @@ -30,6 +30,7 @@ then # Remember that 1st argument is a file path relative to # the file specified in the second argument. ln -sf general-$1.yml conf/general.yml + touch conf/general.yml else echo "File conf/general-$1.yml does not exist." fi diff --git a/bin/update-schema b/bin/update-schema index 2ae374e61..9aff9ec5b 100755 --- a/bin/update-schema +++ b/bin/update-schema @@ -212,6 +212,10 @@ else { # (assuming schema change files are never half-applied, which should be the case) sub get_db_version { return 'EMPTY' if ! table_exists('problem'); + return '0066' if column_exists('users', 'area_ids'); + return '0065' if constraint_contains('admin_log_object_type_check', 'moderation'); + return '0064' if index_exists('moderation_original_data_problem_id_comment_id_idx'); + return '0063' if column_exists('moderation_original_data', 'extra'); return '0062' if column_exists('users', 'created'); return '0061' if column_exists('body', 'extra'); return '0060' if column_exists('body', 'convert_latlong'); @@ -319,3 +323,9 @@ sub function_exists { my $fn = shift; return $db->dbh->selectrow_array('select count(*) from pg_proc where proname = ?', {}, $fn); } + +# Returns true if an index exists +sub index_exists { + my $idx = shift; + return $db->dbh->selectrow_array('select count(*) from pg_indexes where indexname = ?', {}, $idx); +} |