aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet/Cobrand
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet/Cobrand')
-rw-r--r--perllib/FixMyStreet/Cobrand/Barnet.pm80
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm936
-rw-r--r--perllib/FixMyStreet/Cobrand/EmptyHomes.pm178
-rw-r--r--perllib/FixMyStreet/Cobrand/FiksGataMi.pm259
-rw-r--r--perllib/FixMyStreet/Cobrand/Southampton.pm80
5 files changed, 1533 insertions, 0 deletions
diff --git a/perllib/FixMyStreet/Cobrand/Barnet.pm b/perllib/FixMyStreet/Cobrand/Barnet.pm
new file mode 100644
index 000000000..a12fa6d06
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Barnet.pm
@@ -0,0 +1,80 @@
+package FixMyStreet::Cobrand::Barnet;
+use base 'FixMyStreet::Cobrand::Default';
+
+use strict;
+use warnings;
+
+use Carp;
+use URI::Escape;
+use mySociety::VotingArea;
+
+sub site_restriction {
+ return ( "and council='2489'", 'barnet', { council => '2489' } );
+}
+
+sub problems_clause {
+ return { council => '2489' };
+}
+
+sub problems {
+ my $self = shift;
+ return $self->{c}->model('DB::Problem')->search( $self->problems_clause );
+}
+
+sub base_url {
+ my $base_url = mySociety::Config::get('BASE_URL');
+ if ( $base_url !~ /barnet/ ) {
+ $base_url =~ s{http://(?!www\.)}{http://barnet.}g;
+ $base_url =~ s{http://www\.}{http://barnet.}g;
+ }
+ return $base_url;
+}
+
+sub site_title {
+ my ($self) = @_;
+ return 'Barnet Council FixMyStreet';
+}
+
+sub enter_postcode_text {
+ my ($self) = @_;
+ return 'Enter a Barnet postcode, or street name and area';
+}
+
+sub council_check {
+ my ( $self, $params, $context ) = @_;
+
+ my $councils = $params->{all_councils};
+ my $council_match = defined $councils->{2489};
+ if ($council_match) {
+ return 1;
+ }
+ my $url = 'http://www.fixmystreet.com/';
+ $url .= 'alert' if $context eq 'alert';
+ $url .= '?pc=' . URI::Escape::uri_escape( $self->{c}->req->param('pc') )
+ if $self->{c}->req->param('pc');
+ my $error_msg = "That location is not covered by Barnet.
+Please visit <a href=\"$url\">the main FixMyStreet site</a>.";
+ return ( 0, $error_msg );
+}
+
+# All reports page only has the one council.
+sub all_councils_report {
+ return 0;
+}
+
+sub disambiguate_location {
+ return {
+ centre => '51.612832,-0.218169',
+ span => '0.0563,0.09',
+ bounds => [ '51.584682,-0.263169', '51.640982,-0.173169' ],
+ };
+}
+
+sub recent_photos {
+ my ( $self, $num, $lat, $lon, $dist ) = @_;
+ $num = 2 if $num == 3;
+ return $self->problems->recent_photos( $num, $lat, $lon, $dist );
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
new file mode 100644
index 000000000..134111076
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -0,0 +1,936 @@
+package FixMyStreet::Cobrand::Default;
+
+use strict;
+use warnings;
+use FixMyStreet;
+use URI;
+
+use Carp;
+use mySociety::MaPit;
+use mySociety::PostcodeUtil;
+
+=head2 new
+
+ my $cobrand = $class->new;
+ my $cobrand = $class->new( { c => $c } );
+
+Create a new cobrand object, optionally setting the context.
+
+You probably shouldn't need to do this and should get the cobrand object via a
+method in L<FixMyStreet::Cobrand> instead.
+
+=cut
+
+sub new {
+ my $class = shift;
+ my $self = shift || {};
+ return bless $self, $class;
+}
+
+=head2 moniker
+
+ $moniker = $cobrand_class->moniker();
+
+Returns a moniker that can be used to identify this cobrand. By default this is
+the last part of the class name lowercased - eg 'F::C::SomeCobrand' becomes
+'somecobrand'.
+
+=cut
+
+sub moniker {
+ my $class = ref( $_[0] ) || $_[0]; # deal with object or class
+ my ($last_part) = $class =~ m{::(\w+)$};
+ $last_part = lc($last_part);
+ return '' if $last_part eq 'default';
+ return $last_part;
+}
+
+=head2 is_default
+
+ $bool = $cobrand->is_default();
+
+Returns true if this is the default cobrand, false otherwise.
+
+=cut
+
+sub is_default {
+ my $self = shift;
+ return $self->moniker eq '';
+}
+
+=head2 path_to_web_templates
+
+ $path = $cobrand->path_to_web_templates( );
+
+Returns the path to the templates for this cobrand - by default
+"templates/web/$moniker"
+
+=cut
+
+sub path_to_web_templates {
+ my $self = shift;
+ return FixMyStreet->path_to( 'templates/web', $self->moniker );
+}
+
+=head1 country
+
+Returns the country that this cobrand operates in, as an ISO3166-alpha2 code.
+
+=cut
+
+sub country {
+ return 'GB';
+}
+
+=head1 problems_clause
+
+Returns a hash for a query to be used by problems (and elsewhere in joined
+queries) to restrict results for a cobrand.
+
+=cut
+
+sub problems_clause {}
+
+=head1 problems
+
+Returns a ResultSet of Problems, restricted to a subset if we're on a cobrand
+that only wants some of the data.
+
+=cut
+
+sub problems {
+ my $self = shift;
+ return $self->{c}->model('DB::Problem');
+}
+
+=head1 site_restriction
+
+Return a site restriction clause and a site key if the cobrand uses a subset of
+the FixMyStreet data. Parameter is any extra data the cobrand needs. Returns an
+empty string and site key 0 if the cobrand uses all the data.
+
+=cut
+
+sub site_restriction { return ( "", 0, {} ) }
+
+=head2 contact_restriction
+
+Return a contact restriction clause if the cobrand uses a subset of the
+FixMyStreet contact data.
+
+=cut
+
+sub contact_restriction {
+ {};
+}
+
+=head2 restriction
+
+Return a restriction to pull out data saved while using the cobrand site.
+
+=cut
+
+sub restriction {
+ my $self = shift;
+
+ return $self->moniker ? { cobrand => $self->moniker } : {};
+}
+
+=head2 base_url_for_emails
+
+Return the base url to use in links in emails for the cobranded version of the
+site, parameter is extra data.
+
+=cut
+
+sub base_url_for_emails {
+ my $self = shift;
+ return $self->base_url;
+}
+
+=head2 base_url_with_lang
+
+=cut
+
+sub base_url_with_lang {
+ my $self = shift;
+ my $email = shift;
+
+ if ($email) {
+ return $self->base_url_for_emails;
+ } else {
+ return $self->base_url;
+ }
+}
+
+=head2 admin_base_url
+
+Base URL for the admin interface.
+
+=cut
+
+sub admin_base_url { 0 }
+
+=head2 writetothem_url
+
+URL for writetothem; parameter is COBRAND_DATA.
+
+=cut
+
+sub writetothem_url { 0 }
+
+=head2 base_url
+
+Return the base url for the cobranded version of the site
+
+=cut
+
+sub base_url { mySociety::Config::get('BASE_URL') }
+
+=head2 base_host
+
+Return the base host for the cobranded version of the site
+
+=cut
+
+sub base_host {
+ my $self = shift;
+ my $uri = URI->new( $self->base_url );
+ return $uri->host;
+}
+
+=head2 enter_postcode_text
+
+Return the text that prompts the user to enter their postcode/place name.
+Parameter is QUERY
+
+=cut
+
+sub enter_postcode_text { '' }
+
+=head2 set_lang_and_domain
+
+ my $set_lang = $cobrand->set_lang_and_domain( $lang, $unicode, $dir )
+
+Set the language and domain of the site based on the cobrand and host.
+
+=cut
+
+sub set_lang_and_domain {
+ my ( $self, $lang, $unicode, $dir ) = @_;
+ my $set_lang = mySociety::Locale::negotiate_language(
+ 'en-gb,English,en_GB', $lang
+ );
+ mySociety::Locale::gettext_domain( 'FixMyStreet', $unicode, $dir );
+ mySociety::Locale::change();
+ return $set_lang;
+}
+
+=head2 alert_list_options
+
+Return HTML for a list of alert options for the cobrand, given QUERY and
+OPTIONS.
+
+=cut
+
+sub alert_list_options { 0 }
+
+=head2 recent_photos
+
+Return N recent photos. If EASTING, NORTHING and DISTANCE are supplied, the
+photos must be attached to problems within DISTANCE of the point defined by
+EASTING and NORTHING.
+
+=cut
+
+sub recent_photos {
+ my $self = shift;
+ return $self->problems->recent_photos(@_);
+}
+
+=head2 recent
+
+Return recent problems on the site.
+
+=cut
+
+sub recent {
+ my ( $self ) = @_;
+ return $self->problems->recent();
+}
+
+=item shorten_recency_if_new_greater_than_fixed
+
+By default we want to shorten the recency so that the numbers are more
+attractive.
+
+=cut
+
+sub shorten_recency_if_new_greater_than_fixed {
+ return 1;
+}
+
+=head2 front_stats_data
+
+Return a data structure containing the front stats information that a template
+can then format.
+
+=cut
+
+sub front_stats_data {
+ my ( $self ) = @_;
+
+ my $recency = '1 week';
+ my $shorter_recency = '3 days';
+
+ my $fixed = $self->problems->recent_fixed();
+ my $updates = $self->problems->number_comments();
+ my $new = $self->problems->recent_new( $recency );
+
+ if ( $new > $fixed && $self->shorten_recency_if_new_greater_than_fixed ) {
+ $recency = $shorter_recency;
+ $new = $self->problems->recent_new( $recency );
+ }
+
+ my $stats = {
+ fixed => $fixed,
+ updates => $updates,
+ new => $new,
+ recency => $recency,
+ };
+
+ return $stats;
+}
+
+=head2 disambiguate_location
+
+Returns disambiguating information available
+
+=cut
+
+sub disambiguate_location {
+ return {
+ country => 'uk',
+ };
+}
+
+=head2 form_elements
+
+Parameters are FORM_NAME, QUERY. Return HTML for any extra needed elements for
+FORM_NAME
+
+=cut
+
+sub form_elements { '' }
+
+=head2 cobrand_data_for_generic_update
+
+Parameter is UPDATE_DATA, a reference to a hash of non-cobranded update data.
+Return cobrand extra data for the update
+
+=cut
+
+sub cobrand_data_for_generic_update { '' }
+
+=head2 cobrand_data_for_generic_update
+
+Parameter is PROBLEM_DATA, a reference to a hash of non-cobranded problem data.
+Return cobrand extra data for the problem
+
+=cut
+
+sub cobrand_data_for_generic_problem { '' }
+
+=head2 extra_problem_data
+
+Parameter is QUERY. Return a string of extra data to be stored with a problem
+
+=cut
+
+sub extra_problem_data { '' }
+
+=head2 extra_update_data
+
+Parameter is QUERY. Return a string of extra data to be stored with an update
+
+=cut
+
+sub extra_update_data { '' }
+
+=head2 extra_alert_data
+
+Parameter is QUERY. Return a string of extra data to be stored with an alert
+
+=cut
+
+sub extra_alert_data { '' }
+
+=head2 extra_data
+
+Given a QUERY, extract any extra data required by the cobrand
+
+=cut
+
+sub extra_data { '' }
+
+=head2 extra_problem_meta_text
+
+Returns any extra text to be displayed with a PROBLEM.
+
+=cut
+
+sub extra_problem_meta_text { '' }
+
+=head2 extra_update_meta_text
+
+Returns any extra text to be displayed with an UPDATE.
+
+=cut
+
+sub extra_update_meta_text { '' }
+
+=head2 uri
+
+Given a URL ($_[1]), QUERY, EXTRA_DATA, return a URL with any extra params
+needed appended to it.
+
+In the default case, if we're using an OpenLayers map, we need to make
+sure zoom is always present if lat/lon are, to stop OpenLayers defaulting
+to null/0.
+
+=cut
+
+sub uri {
+ my ( $self, $uri ) = @_;
+
+ (my $map_class = $FixMyStreet::Map::map_class) =~ s/^FixMyStreet::Map:://;
+ return $uri unless $map_class =~ /OSM|FMS/;
+
+ $uri->query_param( zoom => 3 )
+ if $uri->query_param('lat') && !$uri->query_param('zoom');
+
+ return $uri;
+}
+
+
+=head2 header_params
+
+Return any params to be added to responses
+
+=cut
+
+sub header_params { return {} }
+
+=head2 root_path_js
+
+Parameter is QUERY. Return some js to set the root path from which AJAX queries
+should be made.
+
+=cut
+
+sub root_path_js { 'var root_path = "";' }
+
+=head2 site_title
+
+Return the title to be used in page heads.
+
+=cut
+
+sub site_title { 'FixMyStreet' }
+
+=head2 on_map_list_limit
+
+Return the maximum number of items to be given in the list of reports on the map
+
+=cut
+
+sub on_map_list_limit { return undef; }
+
+=head2 on_map_default_max_pin_age
+
+Return the default maximum age for pins.
+
+=cut
+
+sub on_map_default_max_pin_age { return '6 months'; }
+
+=head2 allow_photo_upload
+
+Return a boolean indicating whether the cobrand allows photo uploads
+
+=cut
+
+sub allow_photo_upload { return 1; }
+
+=head2 allow_crosssell_adverts
+
+Return a boolean indicating whether the cobrand allows the display of crosssell
+adverts
+
+=cut
+
+sub allow_crosssell_adverts { return 1; }
+
+=head2 allow_photo_display
+
+Return a boolean indicating whether the cobrand allows photo display
+
+=cut
+
+sub allow_photo_display { return 1; }
+
+=head2 allow_update_reporting
+
+Return a boolean indication whether users should see links next to updates
+allowing them to report them as offensive.
+
+=cut
+
+sub allow_update_reporting { return 0; }
+
+=head2 geocode_postcode
+
+Given a QUERY, return LAT/LON and/or ERROR.
+
+=cut
+
+sub geocode_postcode {
+ my ( $self, $s ) = @_;
+
+ if ($s =~ /^\d+$/) {
+ return {
+ error => 'FixMyStreet is a UK-based website that currently works in England, Scotland, and Wales. Please enter either a postcode, or a Great British street name and area.'
+ };
+ } elsif (mySociety::PostcodeUtil::is_valid_postcode($s)) {
+ my $location = mySociety::MaPit::call('postcode', $s);
+ if ($location->{error}) {
+ return {
+ error => $location->{code} =~ /^4/
+ ? _('That postcode was not recognised, sorry.')
+ : $location->{error}
+ };
+ }
+ my $island = $location->{coordsyst};
+ if (!$island) {
+ return {
+ error => _("Sorry, that appears to be a Crown dependency postcode, which we don't cover.")
+ };
+ } elsif ($island eq 'I') {
+ return {
+ error => _("We do not currently cover Northern Ireland, I'm afraid.")
+ };
+ }
+ return {
+ latitude => $location->{wgs84_lat},
+ longitude => $location->{wgs84_lon},
+ };
+ }
+ return {};
+}
+
+=head2 geocoded_string_check
+
+Parameters are LOCATION, QUERY. Return a boolean indicating whether the
+string LOCATION passes the cobrands checks.
+
+=cut
+
+sub geocoded_string_check { return 1; }
+
+=head2 find_closest
+
+Used by send-reports to attach nearest things to the bottom of the report
+
+=cut
+
+sub find_closest {
+ my ( $self, $latitude, $longitude ) = @_;
+ my $str = '';
+
+ # Get nearest road-type thing from Bing
+ my $key = mySociety::Config::get('BING_MAPS_API_KEY', '');
+ if ($key) {
+ my $url = "http://dev.virtualearth.net/REST/v1/Locations/$latitude,$longitude?c=en-GB&key=$key";
+ my $j = LWP::Simple::get($url);
+ if ($j) {
+ $j = JSON->new->utf8->allow_nonref->decode($j);
+ if ($j->{resourceSets}[0]{resources}[0]{name}) {
+ $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"),
+ $j->{resourceSets}[0]{resources}[0]{name}) . "\n\n";
+ }
+ }
+ }
+
+ # Get nearest postcode from Matthew's random gazetteer (put in MaPit? Or elsewhere?)
+ my $url = "http://gazetteer.dracos.vm.bytemark.co.uk/point/$latitude,$longitude.json";
+ my $j = LWP::Simple::get($url);
+ if ($j) {
+ $j = JSON->new->utf8->allow_nonref->decode($j);
+ if ($j->{postcode}) {
+ $str .= sprintf(_("Nearest postcode to the pin placed on the map (automatically generated): %s (%sm away)"),
+ $j->{postcode}[0], $j->{postcode}[1]) . "\n\n";
+ }
+ }
+
+ return $str;
+}
+
+=head2 council_check
+
+Paramters are COUNCILS, QUERY, CONTEXT. Return a boolean indicating whether
+COUNCILS pass any extra checks. CONTEXT is where we are on the site.
+
+=cut
+
+sub council_check { return ( 1, '' ); }
+
+=head2 feed_xsl
+
+Return an XSL to be used in rendering feeds
+
+=cut
+
+sub feed_xsl { '/xsl.xsl' }
+
+=head2 all_councils_report
+
+Return a boolean indicating whether the cobrand displays a report of all
+councils
+
+=cut
+
+sub all_councils_report { 1 }
+
+=head2 ask_ever_reported
+
+Return a boolean indicating whether people should be asked whether this is the
+first time they' ve reported a problem
+
+=cut
+
+sub ask_ever_reported { 1 }
+
+=head2 admin_pages
+
+List of names of pages to display on the admin interface
+
+=cut
+
+sub admin_pages { 0 }
+
+=head2 admin_show_creation_graph
+
+Show the problem creation graph in the admin interface
+=cut
+
+sub admin_show_creation_graph { 1 }
+
+=head2 area_types, area_min_generation
+
+The MaPit types this site handles
+
+=cut
+
+sub area_types { return qw(DIS LBO MTD UTA CTY COI); }
+sub area_min_generation { 10 }
+
+=head2 contact_name, contact_email
+
+Return the contact name or email for the cobranded version of the site (to be
+used in emails).
+
+=cut
+
+sub contact_name { $_[0]->get_cobrand_conf('CONTACT_NAME') }
+sub contact_email { $_[0]->get_cobrand_conf('CONTACT_EMAIL') }
+
+=head2 get_cobrand_conf COBRAND KEY
+
+Get the value for KEY from the config file for COBRAND
+
+=cut
+
+sub get_cobrand_conf {
+ my ( $self, $key ) = @_;
+ my $value = undef;
+ my $cobrand_moniker = $self->moniker;
+
+ my $cobrand_config_file =
+ FixMyStreet->path_to("conf/cobrands/$cobrand_moniker/general");
+ my $normal_config_file = FixMyStreet->path_to('conf/general');
+
+ if ( -e $cobrand_config_file ) {
+
+ # FIXME - don't rely on the config file name - should
+ # change mySociety::Config so that it can return values from a
+ # particular config file instead
+ mySociety::Config::set_file("$cobrand_config_file");
+ my $config_key = $key . "_" . uc($cobrand_moniker);
+ $value = mySociety::Config::get( $config_key, undef );
+ mySociety::Config::set_file("$normal_config_file");
+ }
+
+ # If we didn't find a value use one from normal config
+ if ( !defined($value) ) {
+ $value = mySociety::Config::get($key);
+ }
+
+ return $value;
+}
+
+=item email_host
+
+Return if we are the virtual host that sends email for this cobrand
+
+=cut
+
+sub email_host {
+ return 1;
+}
+
+=item remove_redundant_councils
+
+Remove councils whose reports go to another council
+
+=cut
+
+sub remove_redundant_councils {
+ my $self = shift;
+ my $all_councils = shift;
+
+ # Ipswich & St Edmundsbury are responsible for everything in their
+ # areas, not Suffolk
+ delete $all_councils->{2241}
+ if $all_councils->{2446} #
+ || $all_councils->{2443};
+
+ # Norwich is responsible for everything in its areas, not Norfolk
+ delete $all_councils->{2233} #
+ if $all_councils->{2391};
+}
+
+=item filter_all_council_ids_list
+
+Removes any council IDs that we don't need from an array and returns the
+filtered array
+
+=cut
+
+sub filter_all_council_ids_list {
+ my $self = shift;
+ return @_;
+}
+
+=item short_name
+
+Remove extra information from council names for tidy URIs
+
+=cut
+
+sub short_name {
+ my $self = shift;
+ my ($area, $info) = @_;
+ # Special case Durham as it's the only place with two councils of the same name
+ return 'Durham+County' if $area->{name} eq 'Durham County Council';
+ return 'Durham+City' if $area->{name} eq 'Durham City Council';
+
+ my $name = $area->{name};
+ $name =~ s/ (Borough|City|District|County) Council$//;
+ $name =~ s/ Council$//;
+ $name =~ s/ & / and /;
+ $name =~ s{/}{_}g;
+ $name = URI::Escape::uri_escape_utf8($name);
+ $name =~ s/%20/+/g;
+ return $name;
+
+}
+
+=item council_rss_alert_options
+
+Generate a set of options for council rss alerts.
+
+=cut
+
+sub council_rss_alert_options {
+ my $self = shift;
+ my $all_councils = shift;
+ my $c = shift;
+
+ my %councils = map { $_ => 1 } $self->area_types();
+
+ my $num_councils = scalar keys %$all_councils;
+
+ my ( @options, @reported_to_options );
+ if ( $num_councils == 1 or $num_councils == 2 ) {
+ my ($council, $ward);
+ foreach (values %$all_councils) {
+ if ($councils{$_->{type}}) {
+ $council = $_;
+ $council->{short_name} = $self->short_name( $council );
+ ( $council->{id_name} = $council->{short_name} ) =~ tr/+/_/;
+ } else {
+ $ward = $_;
+ $ward->{short_name} = $self->short_name( $ward );
+ ( $ward->{id_name} = $ward->{short_name} ) =~ tr/+/_/;
+ }
+ }
+
+ push @options,
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $council->{id}, $council->{id_name} ),
+ text => sprintf( _('Problems within %s'), $council->{name}),
+ rss_text => sprintf( _('RSS feed of problems within %s'), $council->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $council->{short_name} ),
+ };
+ push @options,
+ {
+ type => 'ward',
+ id => sprintf( 'ward:%s:%s:%s:%s', $council->{id}, $ward->{id}, $council->{id_name}, $ward->{id_name} ),
+ rss_text => sprintf( _('RSS feed of problems within %s ward'), $ward->{name}),
+ text => sprintf( _('Problems within %s ward'), $ward->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $council->{short_name} . '/' . $ward->{short_name} ),
+ } if $ward;
+ } elsif ( $num_councils == 4 ) {
+# # Two-tier council
+ my ($county, $district, $c_ward, $d_ward);
+ foreach (values %$all_councils) {
+ $_->{short_name} = $self->short_name( $_ );
+ ( $_->{id_name} = $_->{short_name} ) =~ tr/+/_/;
+ if ($_->{type} eq 'CTY') {
+ $county = $_;
+ } elsif ($_->{type} eq 'DIS') {
+ $district = $_;
+ } elsif ($_->{type} eq 'CED') {
+ $c_ward = $_;
+ } elsif ($_->{type} eq 'DIW') {
+ $d_ward = $_;
+ }
+ }
+ my $district_name = $district->{name};
+ my $d_ward_name = $d_ward->{name};
+ my $county_name = $county->{name};
+ my $c_ward_name = $c_ward->{name};
+
+ push @options,
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s', $district->{id}, $district->{id_name} ),
+ text => $district_name,
+ rss_text => sprintf( _('RSS feed for %s'), $district_name ),
+ uri => $c->uri_for( '/rss/areas/' . $district->{short_name} )
+ },
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s:%s:%s', $district->{id}, $d_ward->{id}, $district->{id_name}, $d_ward->{id_name} ),
+ text => sprintf( _('%s ward, %s'), $d_ward_name, $district_name ),
+ rss_text => sprintf( _('RSS feed for %s ward, %s'), $d_ward_name, $district_name ),
+ uri => $c->uri_for( '/rss/areas/' . $district->{short_name} . '/' . $d_ward->{short_name} )
+ },
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s', $county->{id}, $county->{id_name} ),
+ text => $county_name,
+ rss_text => sprintf( _('RSS feed for %s'), $county_name ),
+ uri => $c->uri_for( '/rss/areas/' . $county->{short_name} )
+ },
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s:%s:%s', $county->{id}, $c_ward->{id}, $county->{id_name}, $c_ward->{id_name} ),
+ text => sprintf( _('%s ward, %s'), $c_ward_name, $county_name ),
+ rss_text => sprintf( _('RSS feed for %s ward, %s'), $c_ward_name, $county_name ),
+ uri => $c->uri_for( '/rss/areas/' . $county->{short_name} . '/' . $c_ward->{short_name} )
+ };
+
+ push @reported_to_options,
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $district->{id}, $district->{id_name} ),
+ text => $district->{name},
+ rss_text => sprintf( _('RSS feed of %s'), $district->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $district->{short_name} ),
+ },
+ {
+ type => 'ward',
+ id => sprintf( 'ward:%s:%s:%s:%s', $district->{id}, $d_ward->{id}, $district->{id_name}, $d_ward->{id_name} ),
+ rss_text => sprintf( _('RSS feed of %s, within %s ward'), $district->{name}, $d_ward->{name}),
+ text => sprintf( _('%s, within %s ward'), $district->{name}, $d_ward->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $district->{short_name} . '/' . $d_ward->{short_name} ),
+ },
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $county->{id}, $county->{id_name} ),
+ text => $county->{name},
+ rss_text => sprintf( _('RSS feed of %s'), $county->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $county->{short_name} ),
+ },
+ {
+ type => 'ward',
+ id => sprintf( 'ward:%s:%s:%s:%s', $county->{id}, $c_ward->{id}, $county->{id_name}, $c_ward->{id_name} ),
+ rss_text => sprintf( _('RSS feed of %s, within %s ward'), $county->{name}, $c_ward->{name}),
+ text => sprintf( _('%s, within %s ward'), $county->{name}, $c_ward->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $county->{short_name} . '/' . $c_ward->{short_name} ),
+ };
+
+
+ } else {
+ throw Error::Simple('An area with three tiers of council? Impossible! '. join('|',keys %$all_councils));
+ }
+
+ return ( \@options, @reported_to_options ? \@reported_to_options : undef );
+}
+
+=head2 generate_problem_banner
+
+ my $banner = $c->cobrand->generate_problem_banner;
+
+ <p id="[% banner.id %]:>[% banner.text %]</p>
+
+Generate id and text for banner that appears at top of problem page.
+
+=cut
+
+sub generate_problem_banner {
+ my ( $self, $problem ) = @_;
+
+ my $banner = {};
+ if ($problem->state eq 'confirmed' && time() - $problem->lastupdate_local->epoch > 8*7*24*60*60) {
+ $banner->{id} = 'unknown';
+ $banner->{text} = _('This problem is old and of unknown status.');
+ }
+ if ($problem->state eq 'fixed') {
+ $banner->{id} = 'fixed';
+ $banner->{text} = _('This problem has been fixed') . '.';
+ }
+
+ return $banner;
+}
+
+sub reports_council_check {
+ my ( $self, $c, $code ) = @_;
+
+ if ($code =~ /^(\d\d)([a-z]{2})?([a-z]{2})?$/i) {
+ my $area = mySociety::MaPit::call( 'area', uc $code );
+ $c->detach( 'redirect_index' ) if $area->{error}; # Given a bad/old ONS code
+ if (length($code) == 6) {
+ my $council = mySociety::MaPit::call( 'area', $area->{parent_area} );
+ $c->stash->{ward} = $area;
+ $c->stash->{council} = $council;
+ } else {
+ $c->stash->{council} = $area;
+ }
+ $c->detach( 'redirect_area' );
+ }
+}
+
+=head2 default_photo_resize
+
+Size that photos are to be resized to for display. If photos aren't
+to be resized then return 0;
+
+=cut
+
+sub default_photo_resize { return 0; }
+
+1;
+
diff --git a/perllib/FixMyStreet/Cobrand/EmptyHomes.pm b/perllib/FixMyStreet/Cobrand/EmptyHomes.pm
new file mode 100644
index 000000000..eda0b2882
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/EmptyHomes.pm
@@ -0,0 +1,178 @@
+package FixMyStreet::Cobrand::EmptyHomes;
+use base 'FixMyStreet::Cobrand::Default';
+
+use strict;
+use warnings;
+
+use FixMyStreet;
+use mySociety::Locale;
+use Carp;
+
+=item
+
+Return the base url for this cobranded site
+
+=cut
+
+sub base_url {
+ my $base_url = FixMyStreet->config('BASE_URL');
+ if ( $base_url !~ /emptyhomes/ ) {
+ $base_url =~ s/http:\/\//http:\/\/emptyhomes\./g;
+ }
+ return $base_url;
+}
+
+sub admin_base_url {
+ return 'https://secure.mysociety.org/admin/emptyhomes/';
+}
+
+sub area_types {
+ return qw(DIS LBO MTD UTA LGD COI); # No CTY
+}
+
+
+sub base_url_with_lang {
+ my $self = shift;
+ my $email = shift;
+
+ my $base = $self->base_url;
+
+ if ($email) {
+ $base = $self->base_url_for_emails;
+ }
+
+ my $lang = $mySociety::Locale::lang;
+ if ($lang eq 'cy') {
+ $base =~ s{http://}{$&cy.};
+ } else {
+ $base =~ s{http://}{$&en.};
+ }
+ return $base;
+}
+
+=item set_lang_and_domain LANG UNICODE
+
+Set the language and text domain for the site based on the query and host.
+
+=cut
+
+sub set_lang_and_domain {
+ my ( $self, $lang, $unicode, $dir ) = @_;
+ my $set_lang = mySociety::Locale::negotiate_language(
+ 'en-gb,English,en_GB|cy,Cymraeg,cy_GB', $lang );
+ mySociety::Locale::gettext_domain( 'FixMyStreet-EmptyHomes', $unicode,
+ $dir );
+ mySociety::Locale::change();
+ return $set_lang;
+}
+
+=item site_title
+
+Return the title to be used in page heads
+
+=cut
+
+sub site_title {
+ my ($self) = @_;
+ return _('Report Empty Homes');
+}
+
+=item feed_xsl
+
+Return the XSL file path to be used for feeds'
+
+=cut
+
+sub feed_xsl {
+ my ($self) = @_;
+ return '/xsl.eha.xsl';
+}
+
+=item shorten_recency_if_new_greater_than_fixed
+
+For empty homes we don't want to shorten the recency
+
+=cut
+
+sub shorten_recency_if_new_greater_than_fixed {
+ return 0;
+}
+
+=head2 generate_problem_banner
+
+ my $banner = $c->cobrand->generate_problem_banner;
+
+ <p id="[% banner.id %]:>[% banner.text %]</p>
+
+Generate id and text for banner that appears at top of problem page.
+
+=cut
+
+sub generate_problem_banner {
+ my ( $self, $problem ) = @_;
+
+ my $banner = {};
+ if ($problem->state eq 'fixed') {
+ $banner->{id} = 'fixed';
+ $banner->{text} = _('This problem has been fixed') . '.';
+ }
+
+ return $banner;
+}
+
+=head2 default_photo_resize
+
+Size that photos are to be resized to for display. If photos aren't
+to be resized then return 0;
+
+=cut
+
+sub default_photo_resize { return '195x'; }
+
+=item council_rss_alert_options
+
+Generate a set of options for council rss alerts.
+
+=cut
+
+sub council_rss_alert_options {
+ my $self = shift;
+ my $all_councils = shift;
+ my $c = shift;
+
+ my %councils = map { $_ => 1 } $self->area_types();
+
+ my $num_councils = scalar keys %$all_councils;
+
+ my ( @options, @reported_to_options );
+ my ($council, $ward);
+ foreach (values %$all_councils) {
+ $_->{short_name} = $self->short_name( $_ );
+ ( $_->{id_name} = $_->{short_name} ) =~ tr/+/_/;
+ if ($councils{$_->{type}}) {
+ $council = $_;
+ } else {
+ $ward = $_;
+ }
+ }
+
+ push @options, {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $council->{id}, $council->{id_name} ),
+ text => sprintf( _('Problems within %s'), $council->{name}),
+ rss_text => sprintf( _('RSS feed of problems within %s'), $council->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $council->{short_name} ),
+ };
+ push @options, {
+ type => 'ward',
+ id => sprintf( 'ward:%s:%s:%s:%s', $council->{id}, $ward->{id}, $council->{id_name}, $ward->{id_name} ),
+ rss_text => sprintf( _('RSS feed of problems within %s ward'), $ward->{name}),
+ text => sprintf( _('Problems within %s ward'), $ward->{name}),
+ uri => $c->uri_for( '/rss/reports/' . $council->{short_name} . '/' . $ward->{short_name} ),
+ };
+
+ return ( \@options, @reported_to_options ? \@reported_to_options : undef );
+}
+
+1;
+
diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
new file mode 100644
index 000000000..4f3b975b3
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm
@@ -0,0 +1,259 @@
+package FixMyStreet::Cobrand::FiksGataMi;
+use base 'FixMyStreet::Cobrand::Default';
+
+use strict;
+use warnings;
+
+use Carp;
+use mySociety::MaPit;
+use FixMyStreet::Geocode::OSM;
+
+sub country {
+ return 'NO';
+}
+
+sub set_lang_and_domain {
+ my ( $self, $lang, $unicode, $dir ) = @_;
+ my $set_lang = mySociety::Locale::negotiate_language(
+ 'en-gb,English,en_GB|nb,Norwegian,nb_NO', 'nb'
+ );
+ mySociety::Locale::gettext_domain( 'FixMyStreet', $unicode, $dir );
+ mySociety::Locale::change();
+ return $set_lang;
+}
+
+sub enter_postcode_text {
+ my ( $self ) = @_;
+ return _('Enter a nearby postcode, or street name and area');
+}
+
+# Is also adding language parameter
+sub disambiguate_location {
+ return {
+ lang => 'no',
+ country => 'no',
+ };
+}
+
+sub area_types {
+ return ( 'NKO', 'NFY', 'NRA' );
+}
+
+sub area_min_generation {
+ return '';
+}
+
+sub admin_base_url {
+ return 'http://www.fiksgatami.no/admin/';
+}
+
+sub writetothem_url {
+ return 'http://www.norge.no/styresmakter/';
+}
+
+# If lat/lon are present in the URL, OpenLayers will use that to centre the map.
+# Need to specify a zoom to stop it defaulting to null/0.
+sub uri {
+ my ( $self, $uri ) = @_;
+
+ $uri->query_param( zoom => 3 )
+ if $uri->query_param('lat') && !$uri->query_param('zoom');
+
+ return $uri;
+}
+
+sub geocode_postcode {
+ my ( $self, $s ) = @_;
+
+ if ($s =~ /^\d{4}$/) {
+ my $location = mySociety::MaPit::call('postcode', $s);
+ if ($location->{error}) {
+ return {
+ error => $location->{code} =~ /^4/
+ ? _('That postcode was not recognised, sorry.')
+ : $location->{error}
+ };
+ }
+ return {
+ latitude => $location->{wgs84_lat},
+ longitude => $location->{wgs84_lon},
+ };
+ }
+ return {};
+}
+
+sub geocoded_string_check {
+ my ( $self, $s ) = @_;
+ return 1 if $s =~ /, Norge/;
+ return 0;
+}
+
+sub find_closest {
+ my ( $self, $latitude, $longitude ) = @_;
+ return FixMyStreet::Geocode::OSM::closest_road_text( $self, $latitude, $longitude );
+}
+
+# Used by send-reports, calling find_closest, calling OSM geocoding
+sub guess_road_operator {
+ my ( $self, $inforef ) = @_;
+
+ my $highway = $inforef->{highway} || "unknown";
+ my $refs = $inforef->{ref} || "unknown";
+
+ return "Statens vegvesen"
+ if $highway eq "trunk" || $highway eq "primary";
+
+ for my $ref (split(/;/, $refs)) {
+ return "Statens vegvesen"
+ if $ref =~ m/E ?\d+/ || $ref =~ m/Fv\d+/i;
+ }
+ return '';
+}
+
+sub remove_redundant_councils {
+ my $self = shift;
+ my $all_councils = shift;
+
+ # Oslo is both a kommune and a fylke, we only want to show it once
+ delete $all_councils->{301} #
+ if $all_councils->{3};
+}
+
+sub filter_all_council_ids_list {
+ my $self = shift;
+ my @all_councils_ids = @_;
+
+ # as above we only want to show Oslo once
+ return grep { $_ != 301 } @all_councils_ids;
+}
+
+sub short_name {
+ my $self = shift;
+ my ($area, $info) = @_;
+
+ if ($area->{name} =~ /^(Os|Nes|V\xe5ler|Sande|B\xf8|Her\xf8y)$/) {
+ my $parent = $info->{$area->{parent_area}}->{name};
+ return URI::Escape::uri_escape_utf8("$area->{name}, $parent");
+ }
+
+ my $name = $area->{name};
+ $name =~ s/ & / and /;
+ $name = URI::Escape::uri_escape_utf8($name);
+ $name =~ s/%20/+/g;
+ return $name;
+}
+
+sub council_rss_alert_options {
+ my $self = shift;
+ my $all_councils = shift;
+ my $c = shift;
+
+ my ( @options, @reported_to_options, $fylke, $kommune );
+
+ foreach ( values %$all_councils ) {
+ if ( $_->{type} eq 'NKO' ) {
+ $kommune = $_;
+ }
+ else {
+ $fylke = $_;
+ }
+ }
+
+ if ( $fylke->{id} == 3 ) { # Oslo
+ my $short_name = $self->short_name($fylke, $all_councils);
+ ( my $id_name = $short_name ) =~ tr/+/_/;
+
+ push @options,
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $fylke->{id}, $id_name ),
+ rss_text =>
+ sprintf( _('RSS feed of problems within %s'), $fylke->{name} ),
+ text => sprintf( _('Problems within %s'), $fylke->{name} ),
+ uri => $c->uri_for( '/rss/reports', $short_name ),
+ };
+ }
+ else {
+ my $short_kommune_name = $self->short_name($kommune, $all_councils);
+ ( my $id_kommune_name = $short_kommune_name ) =~ tr/+/_/;
+
+ my $short_fylke_name = $self->short_name($fylke, $all_councils);
+ ( my $id_fylke_name = $short_fylke_name ) =~ tr/+/_/;
+
+ push @options,
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s', $kommune->{id}, $id_kommune_name ),
+ rss_text =>
+ sprintf( _('RSS feed of %s'), $kommune->{name} ),
+ text => $kommune->{name},
+ uri => $c->uri_for( '/rss/area', $short_kommune_name ),
+ },
+ {
+ type => 'area',
+ id => sprintf( 'area:%s:%s', $fylke->{id}, $id_fylke_name ),
+ rss_text =>
+ sprintf( _('RSS feed of %s'), $fylke->{name} ),
+ text => $fylke->{name},
+ uri => $c->uri_for( '/rss/area', $short_fylke_name ),
+ };
+
+ push @reported_to_options,
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $kommune->{id}, $id_kommune_name ),
+ rss_text =>
+ sprintf( _('RSS feed of %s'), $kommune->{name} ),
+ text => $kommune->{name},
+ uri => $c->uri_for( '/rss/reports', $short_kommune_name ),
+ },
+ {
+ type => 'council',
+ id => sprintf( 'council:%s:%s', $fylke->{id}, $id_fylke_name ),
+ rss_text =>
+ sprintf( _('RSS feed of %s'), $fylke->{name} ),
+ text => $fylke->{name},
+ uri => $c->uri_for( '/rss/reports/', $short_fylke_name ),
+ };
+ }
+
+ return (
+ \@options, @reported_to_options
+ ? \@reported_to_options
+ : undef
+ );
+
+}
+
+sub reports_council_check {
+ my ( $self, $c, $council ) = @_;
+
+ if ($council eq 'Oslo') {
+
+ # There are two Oslos (kommune and fylke), we only want one of them.
+ $c->stash->{council} = mySociety::MaPit::call('area', 3);
+ return 1;
+
+ } elsif ($council =~ /,/) {
+
+ # Some kommunes have the same name, use the fylke name to work out which.
+ my ($kommune, $fylke) = split /\s*,\s*/, $council;
+ my @area_types = $c->cobrand->area_types;
+ my $areas_k = mySociety::MaPit::call('areas', $kommune, type => \@area_types);
+ my $areas_f = mySociety::MaPit::call('areas', $fylke, type => \@area_types);
+ if (keys %$areas_f == 1) {
+ ($fylke) = values %$areas_f;
+ foreach (values %$areas_k) {
+ if ($_->{name} eq $kommune && $_->{parent_area} == $fylke->{id}) {
+ $c->stash->{council} = $_;
+ return 1;
+ }
+ }
+ }
+ # If we're here, we've been given a bad name.
+ $c->detach( 'redirect_index' );
+
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/Southampton.pm b/perllib/FixMyStreet/Cobrand/Southampton.pm
new file mode 100644
index 000000000..bd461f5e2
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Southampton.pm
@@ -0,0 +1,80 @@
+package FixMyStreet::Cobrand::Southampton;
+use base 'FixMyStreet::Cobrand::Default';
+
+use strict;
+use warnings;
+
+use Carp;
+use URI::Escape;
+use mySociety::VotingArea;
+
+sub site_restriction {
+ return ( "and council='2567'", 'southampton', { council => '2567' } );
+}
+
+sub problems_clause {
+ return { council => '2567' };
+}
+
+sub problems {
+ my $self = shift;
+ return $self->{c}->model('DB::Problem')->search( $self->problems_clause );
+}
+
+sub base_url {
+ my $base_url = mySociety::Config::get('BASE_URL');
+ if ($base_url !~ /southampton/) {
+ $base_url =~ s{http://(?!www\.)}{http://southampton.}g;
+ $base_url =~ s{http://www\.}{http://southampton.}g;
+ }
+ return $base_url;
+}
+
+sub site_title {
+ my ( $self ) = @_;
+ return 'Southampton City Council FixMyStreet';
+}
+
+sub enter_postcode_text {
+ my ( $self ) = @_;
+ return 'Enter a Southampton postcode, or street name and area';
+}
+
+sub council_check {
+ my ( $self, $params, $context ) = @_;
+
+ my $councils = $params->{all_councils};
+ my $council_match = defined $councils->{2567};
+ if ($council_match) {
+ return 1;
+ }
+ my $url = 'http://www.fixmystreet.com/';
+ $url .= 'alert' if $context eq 'alert';
+ $url .= '?pc=' . URI::Escape::uri_escape_utf8($self->{c}->req->param('pc'))
+ if $self->{c}->req->param('pc');
+ my $error_msg = "That location is not covered by Southampton.
+Please visit <a href=\"$url\">the main FixMyStreet site</a>.";
+ return ( 0, $error_msg );
+}
+
+# All reports page only has the one council.
+sub all_councils_report {
+ return 0;
+}
+
+sub disambiguate_location {
+ return {
+ centre => '50.913822,-1.400493',
+ span => '0.084628,0.15701',
+ bounds => [ '50.871508,-1.478998', '50.956136,-1.321988' ],
+ };
+}
+
+sub recent_photos {
+ my ($self, $num, $lat, $lon, $dist) = @_;
+ $num = 2 if $num == 3;
+ return $self->problems->recent_photos( $num, $lat, $lon, $dist );
+}
+
+1;
+