-
-
Notifications
You must be signed in to change notification settings - Fork 272
[TfL] Add scaffold licence application form #5807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
e249d37
4be75e9
23368e1
b5489f0
af270c8
ff4b36b
7ba614e
612addf
2fe140b
4544af1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| package FixMyStreet::App::Controller::Licence; | ||
| use Moose; | ||
| use namespace::autoclean; | ||
|
|
||
| BEGIN { extends 'FixMyStreet::App::Controller::Form' } | ||
|
|
||
| use utf8; | ||
|
|
||
| # Discover and load all form classes in FixMyStreet::App::Form::Licence::* | ||
| my @ALL_FORM_CLASSES; | ||
| BEGIN { | ||
| require Module::Pluggable; | ||
| Module::Pluggable->import( | ||
| search_path => 'FixMyStreet::App::Form::Licence', | ||
| sub_name => '_form_classes', | ||
| require => 1, | ||
| ); | ||
| @ALL_FORM_CLASSES = __PACKAGE__->_form_classes; | ||
| } | ||
|
|
||
| has feature => ( is => 'ro', default => 'licencing_forms' ); | ||
|
|
||
| has index_template => ( is => 'ro', default => 'licence/index.html' ); | ||
|
|
||
| # Build licence types from discovered form classes | ||
| sub licence_types { | ||
| my $self = shift; | ||
| my %types; | ||
| for my $class (@ALL_FORM_CLASSES) { | ||
| next unless $class->can('type') && $class->can('name'); | ||
| my $type = $class->type; | ||
| $types{$type} = { | ||
| class => $class, | ||
| name => $class->name, | ||
| }; | ||
| } | ||
| return \%types; | ||
| } | ||
|
|
||
| # Override parent Form.pm's index to 404 - you must specify a licence type | ||
| # (Without this, the inherited index would try to load a non-existent form) | ||
| sub index : Path : Args(0) { | ||
| my ($self, $c) = @_; | ||
| $c->detach('/page_error_404_not_found'); | ||
| } | ||
|
|
||
| # GET/POST /licence/:type - show/process a specific licence form | ||
| sub show : Path : Args(1) { | ||
| my ($self, $c, $type) = @_; | ||
|
|
||
| my $licence_config = $self->licence_types->{lc $type} | ||
| or $c->detach('/page_error_404_not_found'); | ||
|
|
||
| $c->stash->{form_class} = $licence_config->{class}; | ||
| $c->stash->{licence_type} = lc $type; | ||
| $c->stash->{licence_name} = $licence_config->{name}; | ||
|
|
||
| $c->forward('/auth/get_csrf_token'); | ||
| $c->forward('form'); | ||
| } | ||
|
|
||
| sub process_licence : Private { | ||
| my ($self, $c, $form) = @_; | ||
|
|
||
| my $data = $form->saved_data; | ||
| my $type = $c->stash->{licence_type}; | ||
| my $name = $c->stash->{licence_name}; | ||
|
|
||
| # Handle staff submitting on behalf of another user | ||
| my $contributing_as_another_user = $c->user_exists | ||
| && $c->user->from_body | ||
| && $data->{email} | ||
| && $c->user->email ne $data->{email}; | ||
|
|
||
| # Find or create user | ||
| my $user = $c->user_exists | ||
| ? $c->user->obj | ||
| : $c->model('DB::User')->find_or_new({ email => $data->{email} }); | ||
| $user->name($data->{name}) if $data->{name}; | ||
| $user->phone($data->{phone}) if $data->{phone}; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this may be a problem in Claims as well; if the user already exists, its name/phone are not updated (which I think is right, because otherwise someone could override it only by specifying their email, if there's no confirmation). It looks like in Waste we're now setting a |
||
|
|
||
| # Build detail string from form fields, grouped by section | ||
| my $detail = ""; | ||
| if ($form->can('fields_for_display')) { | ||
dracos marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| my @sections; | ||
| for my $stage (@{ $form->fields_for_display }) { | ||
| next if $stage->{hide}; | ||
| my @visible_fields = grep { !$_->{hide} } @{ $stage->{fields} }; | ||
| next unless @visible_fields; | ||
|
|
||
| my $section = ""; | ||
| $section .= "[$stage->{title}]\n" if $stage->{title}; | ||
| for my $field (@visible_fields) { | ||
| $section .= "$field->{desc}: $field->{pretty}\n"; | ||
| } | ||
| push @sections, $section; | ||
| } | ||
| $detail = join("\n", @sections); | ||
| } | ||
|
|
||
| my $category = "$name licence"; | ||
|
|
||
| # Default to central London (Trafalgar Square) if geocoding didn't provide coordinates. | ||
| # This ensures the report can be viewed without National Grid conversion errors. | ||
| my $latitude = $data->{latitude} || 51.508; | ||
| my $longitude = $data->{longitude} || -0.128; | ||
|
|
||
| my $problem = $c->model('DB::Problem')->new({ | ||
| non_public => 1, | ||
| category => $category, | ||
| used_map => $data->{latitude} ? 1 : 0, | ||
| title => $category, | ||
| detail => $detail, | ||
| postcode => $data->{postcode} || '', | ||
| latitude => $latitude, | ||
| longitude => $longitude, | ||
| areas => '', | ||
| send_questionnaire => 0, | ||
| bodies_str => $c->cobrand->body->id, | ||
| photo => $data->{photos}, | ||
| state => 'unconfirmed', | ||
| cobrand => $c->cobrand->moniker, | ||
| cobrand_data => 'licence', | ||
| lang => $c->stash->{lang_code}, | ||
| user => $user, | ||
| name => $user->name || '', | ||
| anonymous => 0, | ||
| extra => $data, | ||
| }); | ||
|
|
||
| $c->stash->{detail} = $detail; | ||
|
|
||
| # Handle user creation/association | ||
| if ($contributing_as_another_user) { | ||
| $problem->set_extra_metadata(contributed_as => 'another_user'); | ||
| $problem->set_extra_metadata(contributed_by => $c->user->id); | ||
| } elsif (!$problem->user->in_storage) { | ||
| $problem->user->insert(); | ||
| } elsif ($c->user && $problem->user->id == $c->user->id) { | ||
| $problem->user->update(); | ||
| } else { | ||
| $problem->user->discard_changes(); | ||
| } | ||
|
|
||
| $problem->confirm; | ||
| $problem->insert; | ||
| $problem->create_related_things(); | ||
|
|
||
| # Check for auto-response template | ||
| my $template = $problem->response_templates->search({ 'me.state' => $problem->state })->first; | ||
| $c->stash->{auto_response} = $template->text if $template; | ||
|
|
||
| $c->stash->{problem} = $problem; | ||
| $c->stash->{reference} = 'FMS' . $problem->id; | ||
|
|
||
| return 1; | ||
| } | ||
|
|
||
| __PACKAGE__->meta->make_immutable; | ||
|
|
||
| 1; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,11 +3,7 @@ package FixMyStreet::App::Form::Claims; | |
| use HTML::FormHandler::Moose; | ||
| extends 'FixMyStreet::App::Form::Wizard'; | ||
| use utf8; | ||
|
|
||
| use Path::Tiny; | ||
| use File::Copy; | ||
| use Digest::SHA qw(sha1_hex); | ||
| use File::Basename; | ||
|
|
||
| has c => ( is => 'ro' ); | ||
|
|
||
|
|
@@ -17,11 +13,10 @@ has finished_action => ( is => 'ro' ); | |
|
|
||
| has '+is_html5' => ( default => 1 ); | ||
|
|
||
| has upload_dir => ( is => 'ro', default => sub { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than three similar upload_dir functions, you could have the one (in Wizard.pm) that calls $_[0]->upload_subdir instead of "uploads", then each class/subclass only has to set that to claims_files/licence_files etc. |
||
| has '+upload_dir' => ( default => sub { | ||
| my $cfg = FixMyStreet->config('PHOTO_STORAGE_OPTIONS'); | ||
| my $dir = $cfg ? $cfg->{UPLOAD_DIR} : FixMyStreet->config('UPLOAD_DIR'); | ||
| $dir = path($dir, "claims_files")->absolute(FixMyStreet->path_to())->mkdir; | ||
| return $dir; | ||
| path($dir, "claims_files")->absolute(FixMyStreet->path_to())->mkdir; | ||
| }); | ||
|
|
||
| before _process_page_array => sub { | ||
|
|
@@ -1098,26 +1093,6 @@ sub format_for_display { | |
| return $value; | ||
| } | ||
|
|
||
| # params does not include file uploads which causes breaks the | ||
| # validation and value setting so we need to handle them here. | ||
| sub get_params { | ||
| my ($self, $c) = @_; | ||
|
|
||
| my @params = $c->req->body_params; | ||
|
|
||
| if ( $c->req->uploads ) { | ||
| for my $field ( keys %{ $c->req->uploads } ) { | ||
| next unless $self->field($field); | ||
| if ($self->field($field)->{type} eq 'FileIdUpload') { | ||
| $self->file_upload($field); | ||
| $params[0]->{$field} = $self->saved_data->{$field}; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return @params; | ||
| } | ||
|
|
||
| # this makes sure that if any of the child fields have errors we mark the date | ||
| # as invalid, even if it's technically a valid date. This is mostly to catch | ||
| # range errors on the year. Otherwise we get an error at the top of the page | ||
|
|
@@ -1138,47 +1113,4 @@ sub validate_datetime { | |
| $field->add_error("Please enter a valid date") unless $valid; | ||
| } | ||
|
|
||
| sub file_upload { | ||
| my ($form, $field) = @_; | ||
|
|
||
| my $c = $form->{c}; | ||
| my $saved_data = $form->saved_data; | ||
|
|
||
| my $upload = $c->req->upload($field); | ||
| if ( $upload ) { | ||
| FixMyStreet::PhotoStorage::base64_decode_upload($c, $upload); | ||
| my ($p, $n, $ext) = fileparse($upload->filename, qr/\.[^.]*/); | ||
| my $key = sha1_hex($upload->slurp) . $ext; | ||
| my $out = $form->upload_dir->child($key); | ||
| unless (copy($upload->tempname, $out)) { | ||
| $c->log->info('Couldn\'t copy temp file to destination: ' . $!); | ||
| $c->stash->{photo_error} = _("Sorry, we couldn't save your file(s), please try again."); | ||
| return; | ||
| } | ||
| # Then store the file hashes along with the original filenames for display | ||
| $saved_data->{$field} = { files => $key, filenames => [ $upload->raw_basename ] }; | ||
| } | ||
| } | ||
|
|
||
| sub handle_upload { | ||
| my ($form, $field, $fields) = @_; | ||
|
|
||
| my $saved_data = $form->saved_data; | ||
| if ( $saved_data->{$field} ) { | ||
| $fields->{$field} = { default => $saved_data->{$field}->{files}, tags => $saved_data->{$field} }; | ||
| } | ||
| } | ||
|
|
||
| sub process_upload { | ||
| my ($form, $field) = @_; | ||
|
|
||
| my $saved_data = $form->saved_data; | ||
| my $c = $form->{c}; | ||
|
|
||
| if ( !$saved_data->{$field} && $c->req->params->{$field . '_fileid'} ) { | ||
| # The data was already passed in from when it was saved before (also in tags, from above) | ||
| $saved_data->{$field} = $form->field($field)->init_value; | ||
| } | ||
| } | ||
|
|
||
| 1; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package FixMyStreet::App::Form::Licence; | ||
|
|
||
| use Moose::Role; | ||
| use Path::Tiny; | ||
|
|
||
| =head1 NAME | ||
| FixMyStreet::App::Form::Licence - Role for TfL licence application forms | ||
| =head1 DESCRIPTION | ||
| This role provides shared functionality for all licence application forms, | ||
| including summary display methods. | ||
| File upload methods (file_upload, handle_upload, process_upload) are provided | ||
| by the base Wizard class. | ||
| =cut | ||
|
|
||
| # Upload directory for licence files (separate from claims) | ||
| # Note: This shadows the upload_dir from Wizard.pm when this role is composed | ||
| has upload_dir => ( is => 'ro', lazy => 1, default => sub { | ||
| my $cfg = FixMyStreet->config('PHOTO_STORAGE_OPTIONS'); | ||
| my $dir = $cfg ? $cfg->{UPLOAD_DIR} : FixMyStreet->config('UPLOAD_DIR'); | ||
| path($dir, "licence_files")->absolute(FixMyStreet->path_to())->mkdir; | ||
| }); | ||
|
|
||
| =head2 fields_for_display | ||
| Returns an array of pages with their fields, formatted for display on the | ||
| summary page. Each page contains: | ||
| { | ||
| stage => 'page_name', | ||
| title => 'Page Title', | ||
| hide => 0/1, # optional | ||
| fields => [ | ||
| { | ||
| name => 'field_name', | ||
| desc => 'Field Label', | ||
| type => 'Text', | ||
| pretty => 'Formatted value', | ||
| value => 'raw_value', | ||
| hide => 0/1, # optional | ||
| }, | ||
| ... | ||
| ] | ||
| } | ||
| =cut | ||
|
|
||
| sub fields_for_display { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this and format for display be shared with Claims? They seem pretty almost identical. |
||
| my ($form) = @_; | ||
|
|
||
| my $things = []; | ||
| for my $page ( @{ $form->pages } ) { | ||
| my $x = { | ||
| stage => $page->{name}, | ||
| title => $page->{title}, | ||
| ( $page->tag_exists('hide') ? ( hide => $page->get_tag('hide') ) : () ), | ||
| fields => [] | ||
| }; | ||
|
|
||
| for my $f ( @{ $page->fields } ) { | ||
| my $field = $form->field($f); | ||
| next if $field->type eq 'Submit'; | ||
| my $value = $form->saved_data->{$field->{name}} // ''; | ||
| push @{$x->{fields}}, { | ||
| name => $field->{name}, | ||
| desc => $field->{label}, | ||
| type => $field->type, | ||
| pretty => $form->format_for_display( $field->{name}, $value ), | ||
| value => $value, | ||
| ( $field->tag_exists('hide') ? ( hide => $field->get_tag('hide') ) : () ), | ||
| }; | ||
| } | ||
|
|
||
| push @$things, $x; | ||
| } | ||
|
|
||
| return $things; | ||
| } | ||
|
|
||
| =head2 format_for_display | ||
| Converts a field value to a human-readable format for display. | ||
| Handles special cases: | ||
| - Select fields: returns the label for the selected value | ||
| - DateTime fields: formats as day/month/year | ||
| - Checkbox fields: returns 'Yes' or 'No' | ||
| - FileIdUpload fields: returns comma-separated filenames | ||
| =cut | ||
|
|
||
| sub format_for_display { | ||
| my ($form, $field_name, $value) = @_; | ||
| my $field = $form->field($field_name); | ||
|
|
||
| if ( $field->{type} eq 'Select' ) { | ||
| # Find label for the selected value | ||
| for my $opt (@{$field->options}) { | ||
| return $opt->{label} if defined $opt->{value} && $opt->{value} eq $value; | ||
| } | ||
| return $value; | ||
| } elsif ( $field->{type} eq 'DateTime' ) { | ||
| if ( ref $value eq 'DateTime' ) { | ||
| return join( '/', $value->day, $value->month, $value->year); | ||
| } elsif ( ref $value eq 'HASH' && $value->{day} ) { | ||
| return "$value->{day}/$value->{month}/$value->{year}"; | ||
| } | ||
| return ""; | ||
| } elsif ( $field->{type} eq 'Checkbox' ) { | ||
| return $value ? 'Yes' : 'No'; | ||
| } elsif ( $field->{type} eq 'FileIdUpload' ) { | ||
| if ( ref $value eq 'HASH' && $value->{filenames} ) { | ||
| return join( ', ', @{ $value->{filenames} } ); | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| return $value // ''; | ||
| } | ||
|
|
||
| 1; | ||
Uh oh!
There was an error while loading. Please reload this page.