From: Moritz Bunkus Date: Tue, 16 Apr 2013 13:49:59 +0000 (+0200) Subject: Pflichtenhefte: Zeit- und Kostenschätzungsmaske X-Git-Tag: release-3.2.0beta~467^2~198 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=c19b1e03fb03f195d86a5b78f8ce2338f745f599;p=kivitendo-erp.git Pflichtenhefte: Zeit- und Kostenschätzungsmaske --- diff --git a/SL/Controller/Helper/RequirementSpec.pm b/SL/Controller/Helper/RequirementSpec.pm new file mode 100644 index 000000000..b24c4aa80 --- /dev/null +++ b/SL/Controller/Helper/RequirementSpec.pm @@ -0,0 +1,25 @@ +package SL::Controller::Helper::RequirementSpec; + +use strict; + +use Exporter qw(import); +our @EXPORT = qw(init_visible_section); + +use SL::DB::Manager::RequirementSpecItem; + +sub init_visible_section { + my ($self) = @_; + + my $content_id = $::form->{current_content_id}; + my $content_type = $::form->{current_content_type}; + + return undef unless $content_id; + return undef unless $content_type =~ m/section|function-block/; + + $self->visible_item(SL::DB::Manager::RequirementSpecItem->find_by(id => $content_id)); + return undef unless $self->visible_item; + + return $self->visible_section($self->visible_item->section); +} + +1; diff --git a/SL/Controller/RequirementSpec.pm b/SL/Controller/RequirementSpec.pm index 5d581f4fb..48af731e5 100644 --- a/SL/Controller/RequirementSpec.pm +++ b/SL/Controller/RequirementSpec.pm @@ -10,8 +10,11 @@ use SL::Controller::Helper::Paginated; use SL::Controller::Helper::Sorted; use SL::Controller::Helper::ParseFilter; use SL::Controller::Helper::ReportGenerator; +use SL::Controller::Helper::RequirementSpec; use SL::DB::Customer; use SL::DB::Project; +use SL::DB::RequirementSpecComplexity; +use SL::DB::RequirementSpecRisk; use SL::DB::RequirementSpecStatus; use SL::DB::RequirementSpecType; use SL::DB::RequirementSpec; @@ -20,13 +23,12 @@ use SL::Locale::String; use Rose::Object::MakeMethods::Generic ( - scalar => [ qw(requirement_spec requirement_spec_item customers projects types statuses db_args flat_filter is_template) ], + scalar => [ qw(requirement_spec_item customers types statuses db_args flat_filter is_template visible_item visible_section) ], + 'scalar --get_set_init' => [ qw(requirement_spec complexities risks projects) ], ); __PACKAGE__->run_before('setup'); -__PACKAGE__->run_before('load_requirement_spec', only => [ qw( ajax_edit update show destroy) ]); -__PACKAGE__->run_before('load_select_options', only => [ qw(new ajax_edit create update list) ]); -__PACKAGE__->run_before('load_search_select_options', only => [ qw( list) ]); +__PACKAGE__->run_before('load_select_options', only => [ qw(new ajax_edit create update list) ]); __PACKAGE__->get_models_url_params('flat_filter'); __PACKAGE__->make_paginated( @@ -53,6 +55,7 @@ __PACKAGE__->make_sorted( # actions # + sub action_list { my ($self) = @_; @@ -79,6 +82,59 @@ sub action_ajax_edit { $self->render('requirement_spec/_form', { layout => 0 }, submit_as => 'ajax'); } +sub action_ajax_show_time_and_cost_estimate { + my ($self) = @_; + + $self->render('requirement_spec/_show_time_and_cost_estimate', { layout => 0 }); +} + +sub action_ajax_cancel_time_and_cost_estimate { + my ($self) = @_; + + my $html = $self->render('requirement_spec/_show_time_and_cost_estimate', { output => 0 }); + + SL::ClientJS->new + ->replaceWith('#time_cost_estimate', $html) + ->render($self); +} + +sub action_ajax_edit_time_and_cost_estimate { + my ($self) = @_; + + my $html = $self->render('requirement_spec/_edit_time_and_cost_estimate', { output => 0 }); + + SL::ClientJS->new + ->replaceWith('#time_cost_estimate', $html) + ->render($self); +} + +sub action_ajax_save_time_and_cost_estimate { + my ($self) = @_; + + $self->requirement_spec->db->do_transaction(sub { + # Make Emacs happy + 1; + foreach my $attributes (@{ $::form->{requirement_spec_items} || [] }) { + SL::DB::RequirementSpecItem + ->new(id => delete $attributes->{id}) + ->load + ->update_attributes(%{ $attributes }); + } + + 1; + }); + + my $html = $self->render('requirement_spec/_show_time_and_cost_estimate', { output => 0 }); + my $js = SL::ClientJS->new->replaceWith('#time_cost_estimate', $html); + + if ($self->visible_section) { + $html = $self->render('requirement_spec_item/_section', { output => 0 }, requirement_spec_item => $self->visible_section); + $js->html('#column-content', $html); + } + + $js->render($self); +} + sub action_show { my ($self) = @_; @@ -131,34 +187,42 @@ sub setup { $::request->{layout}->use_stylesheet("${_}.css") for qw(jquery.contextMenu requirement_spec); $::request->{layout}->use_javascript("${_}.js") for qw(jquery.jstree jquery/jquery.contextMenu client_js requirement_spec); $self->is_template($::form->{is_template} ? 1 : 0); + $self->init_visible_section; return 1; } -sub load_requirement_spec { +sub init_complexities { + my ($self) = @_; + return SL::DB::Manager::RequirementSpecComplexity->get_all_sorted; +} + +sub init_risks { + my ($self) = @_; + return SL::DB::Manager::RequirementSpecRisk->get_all_sorted; +} + +sub init_projects { + my ($self) = @_; + $self->projects(SL::DB::Manager::Project->get_all_sorted); +} + +sub init_requirement_spec { my ($self) = @_; - $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{id})->load || die "No such requirement spec"); + $self->requirement_spec(SL::DB::RequirementSpec->new(id => $::form->{id})->load || die "No such requirement spec") if $::form->{id}; } sub load_select_options { my ($self) = @_; my @filter = ('!obsolete' => 1); - if ($self->requirement_spec && $self->requirement_spec->customer_id) { - @filter = ( or => [ @filter, id => $self->requirement_spec->customer_id ] ); - } + @filter = ( or => [ @filter, id => $self->requirement_spec->customer_id ] ) if $self->requirement_spec && $self->requirement_spec->customer_id; $self->customers(SL::DB::Manager::Customer->get_all_sorted(where => \@filter)); $self->statuses( SL::DB::Manager::RequirementSpecStatus->get_all_sorted); $self->types( SL::DB::Manager::RequirementSpecType->get_all_sorted); } -sub load_search_select_options { - my ($self) = @_; - - $self->projects(SL::DB::Manager::Project->get_all_sorted); -} - # # helpers # diff --git a/SL/Controller/RequirementSpecItem.pm b/SL/Controller/RequirementSpecItem.pm index d4f3b8f0e..7cad81151 100644 --- a/SL/Controller/RequirementSpecItem.pm +++ b/SL/Controller/RequirementSpecItem.pm @@ -10,6 +10,7 @@ use List::Util qw(first); use Time::HiRes (); use SL::Clipboard; +use SL::Controller::Helper::RequirementSpec; use SL::DB::RequirementSpec; use SL::DB::RequirementSpecComplexity; use SL::DB::RequirementSpecItem; @@ -490,21 +491,6 @@ sub format_exception { return join "\n", (split m/\n/, $@)[0..4]; } -sub init_visible_section { - my ($self) = @_; - - my $content_id = $::form->{current_content_id}; - my $content_type = $::form->{current_content_type}; - - return undef unless $content_id; - return undef unless $content_type =~ m/section|function-block/; - - $self->visible_item(SL::DB::Manager::RequirementSpecItem->find_by(id => $content_id)); - return undef unless $self->visible_item; - - return $self->visible_section($self->visible_item->section); -} - sub init_complexities { my ($self) = @_; diff --git a/SL/Controller/Test.pm b/SL/Controller/Test.pm new file mode 100644 index 000000000..30638a18a --- /dev/null +++ b/SL/Controller/Test.pm @@ -0,0 +1,17 @@ +package SL::Controller::Test; + +use strict; + +use parent qw(SL::Controller::Base); + +use Data::Dumper; +use SL::ClientJS; + +sub action_dump_form { + my ($self) = @_; + + my $output = Dumper($::form); + $self->render(\$output, { type => 'text' }); +} + +1; diff --git a/css/requirement_spec.css b/css/requirement_spec.css index d5edd4bee..45628179c 100644 --- a/css/requirement_spec.css +++ b/css/requirement_spec.css @@ -43,8 +43,9 @@ table.rs_input_field input, table.rs_input_field select { /* Special things that apply to the context menu */ /* ------------------------------------------------------------ */ -.context-menu-item.icon-flag { background-image: url("../image/flag-red.png"); } - +.context-menu-item.icon-flag { background-image: url("../image/flag-red.png"); } +.context-menu-item.icon-close { background-image: url("../image/document-close.png"); } +.context-menu-item.icon-save { background-image: url("../image/document-save.png"); } /* ------------------------------------------------------------ */ /* Sections & function blocks */ diff --git a/js/locale/de.js b/js/locale/de.js index d1f1ef816..5da04ec00 100644 --- a/js/locale/de.js +++ b/js/locale/de.js @@ -5,21 +5,23 @@ namespace("kivi").setupLocale({ "Add sub function block":"Unterfunktionsblock hinzufügen", "Add text block":"Textblock erfassen", "Are you sure?":"Sind Sie sicher?", +"Copy":"Kopieren", "Database Connection Test":"Test der Datenbankverbindung", +"Delete text block":"Textblock löschen", +"Delete":"Löschen", +"Do you really want to cancel?":"Wollen Sie wirklich abbrechen?", "Do you want to set the account number \"#1\" to \"#2\" and the name \"#3\" to \"#4\"?":"Soll die Kontonummer \"#1\" zu \"#2\" und den Name \"#3\" zu \"#4\" geändert werden?", +"Edit text block":"Textblock bearbeiten", +"Edit":"Bearbeiten", "Enter longdescription":"Langtext eingeben", "Map":"Karte", "Part picker":"Artikelauswahl", +"Paste":"Einfügen", +"Save":"Speichern", "The description is missing.":"Die Beschreibung fehlt.", "The name is missing.":"Der Name fehlt.", "The name must only consist of letters, numbers and underscores and start with a letter.":"Der Name darf nur aus Buchstaben (keine Umlaute), Ziffern und Unterstrichen bestehen und muss mit einem Buchstaben beginnen.", "The option field is empty.":"Das Optionsfeld ist leer.", "The selected database is still configured for client \"#1\". If you delete the database that client will stop working until you re-configure it. Do you still want to delete the database?":"Die auswählte Datenbank ist noch für Mandant \"#1\" konfiguriert. Wenn Sie die Datenbank löschen, wird der Mandanten nicht mehr funktionieren, bis er anders konfiguriert wurde. Wollen Sie die Datenbank trotzdem löschen?" -"Copy":"Kopieren", -"Delete":"Löschen", -"Delete text block":"Textblock löschen", -"Edit":"Bearbeiten", -"Edit text block":"Textblock bearbeiten", -"Paste":"Einfügen", "Toggle marker":"Markierung umschalten" }); diff --git a/js/requirement_spec.js b/js/requirement_spec.js index 618ffb1e7..a259682a1 100644 --- a/js/requirement_spec.js +++ b/js/requirement_spec.js @@ -268,6 +268,32 @@ function requirement_spec_item_popup_menu_hidden(opt) { return handle_item_popup_menu_markings(opt, false); } +// ------------------------------------------------------------------------- +// -------------------------- time/cost estimate --------------------------- +// ------------------------------------------------------------------------- + +function standard_time_cost_estimate_ajax_call(key, opt) { + if ((key == 'cancel') && !confirm(kivi.t8('Do you really want to cancel?'))) + return true; + + var data = "action=RequirementSpec/ajax_" + key + "_time_and_cost_estimate&"; + + if (key == 'save') + data += $('#edit_time_cost_estimate_form').serialize() + + '&' + $('#current_content_type').serialize() + + '&' + $('#current_content_id').serialize(); + else + data += 'id=' + encodeURIComponent($('#requirement_spec_id').val()); + + $.post("controller.pl", data, eval_json_result); + + return true; +} + +// ------------------------------------------------------------------------- +// ----------------------------- context menus ----------------------------- +// ------------------------------------------------------------------------- + function create_requirement_spec_context_menus() { var events = { show: requirement_spec_text_block_popup_menu_shown, @@ -330,4 +356,19 @@ function create_requirement_spec_context_menus() { , paste: { name: kivi.t8('Paste'), icon: "paste", callback: standard_item_ajax_call } } }); + + $.contextMenu({ + selector: '.time-cost-estimate-context-menu', + events: events, + items: { edit: { name: kivi.t8('Edit'), icon: "edit", callback: standard_time_cost_estimate_ajax_call } } + }); + + $.contextMenu({ + selector: '.edit-time-cost-estimate-context-menu', + events: events, + items: { + save: { name: kivi.t8('Save'), icon: "save", callback: standard_time_cost_estimate_ajax_call } + , cancel: { name: kivi.t8('Cancel'), icon: "close", callback: standard_time_cost_estimate_ajax_call } + } + }); } diff --git a/locale/de/all b/locale/de/all index 0f7f91835..da52e721b 100755 --- a/locale/de/all +++ b/locale/de/all @@ -514,6 +514,7 @@ $self->{texts} = { 'Copy file from #1 to #2 failed: #3' => 'Kopieren der Datei von #1 nach #2 schlug fehl: #3', 'Copy' => 'Kopieren', 'Correct taxkey' => 'Richtiger Steuerschlüssel', + 'Cost' => 'Kosten', 'Costs' => 'Kosten', 'Could not load class #1 (#2): "#3"' => 'Konnte Klasse #1 (#2) nicht laden: "#3"', 'Could not load class #1, #2' => 'Konnte Klasse #1 nicht laden: "#2"', @@ -765,6 +766,7 @@ $self->{texts} = { 'Do not change the tax rate of taxkey 0.' => 'Ändern Sie nicht den Steuersatz vom Steuerschlüssel 0.', 'Do not check for duplicates' => 'Nicht nach Dubletten suchen', 'Do not set default buchungsgruppe' => 'Nie Standardbuchungsgruppe setzen', + 'Do you really want to cancel?' => 'Wollen Sie wirklich abbrechen?', 'Do you really want to close the following SEPA exports? No payment will be recorded for bank collections that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.', 'Do you really want to close the following SEPA exports? No payment will be recorded for bank transfers that haven\'t been marked as executed yet.' => 'Wollen Sie wirklich die folgenden SEPA-Exporte abschließen? Für Überweisungen, die noch nicht gebucht wurden, werden dann keine Zahlungen verbucht.', 'Do you really want to delete AP transaction #1?' => 'Wollen Sie wirklich die Kreditorenbuchung #1 löschen?', @@ -1064,6 +1066,7 @@ $self->{texts} = { 'Full Access' => 'Vollzugriff', 'Full Preview' => 'Alles', 'Full access to all functions' => 'Vollzugriff auf alle Funktionen', + 'Function block' => 'Funktionsblock', 'Function/position' => 'Funktion/Position', 'Fwd' => 'Vorwärts', 'GL Transaction' => 'Dialogbuchung', @@ -1395,6 +1398,7 @@ $self->{texts} = { 'Name and Street' => 'Name und Straße', 'National Expenses' => 'Aufwand Inland', 'National Revenues' => 'Erlöse Inland', + 'Neither sections nor function blocks have been created yet.' => 'Es wurden bisher weder Abschnitte noch Funktionsblöcke angelegt.', 'Net Income Statement' => 'Einnahmenüberschußrechnung', 'Net amount' => 'Nettobetrag', 'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)', @@ -2091,6 +2095,8 @@ $self->{texts} = { 'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => 'Solche Einträge sind aber nicht DATEV-exportiertbar und müssen ebenfalls korrigiert werden.', 'Sum Credit' => 'Summe Haben', 'Sum Debit' => 'Summe Soll', + 'Sum for #1' => 'Summe für #1', + 'Sum for section' => 'Summe für Abschnitt', 'Sum for' => 'Summe für', 'Sum open amount' => 'Summierter offener Betrag', 'Sum per' => 'Summe per', @@ -2459,6 +2465,7 @@ $self->{texts} = { 'Three Options:' => 'Drei Optionen:', 'Time Format' => 'Uhrzeitformat', 'Time Tracking' => 'Zeiterfassung', + 'Time and cost estimate' => 'Zeit- und Kostenschätzung', 'Time estimate' => 'Zeitschätzung', 'Time period for the analysis:' => 'Analysezeitraum:', 'Timestamp' => 'Uhrzeit', diff --git a/templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html new file mode 100644 index 000000000..f0e6831a9 --- /dev/null +++ b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate.html @@ -0,0 +1,44 @@ +[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE P -%] +[%- DEFAULT id_prefix = 'time_and_cost_estimate_form' %] + +
+ [%- IF !SELF.requirement_spec.sections.size %] +

[%- LxERP.t8("Neither sections nor function blocks have been created yet.") %]

+ + [%- ELSE %] + + [%- SET at_least_one_function_block = 0 %] + +
+ [%- L.hidden_tag('id', SELF.requirement_spec.id, id=id_prefix _ '_id') -%] + + [%# time-cost-estimate-context-menu %] + + + [%- FOREACH section = SELF.requirement_spec.sections %] + + + + + + + + + + + + [%- IF section.children.size %] + [%- SET at_least_one_function_block = 1 %] + [%- FOREACH child = section.children %] + [%- INCLUDE 'requirement_spec/_edit_time_and_cost_estimate_item.html' + id_prefix = id_prefix + item = child + level = 1 %] + [%- END %] + [%- END -%] + [%- END -%] + +
[%- LxERP.t8("Function block") %][%- LxERP.t8("Complexity") %][%- LxERP.t8("Risk") %][%- LxERP.t8("Time estimate") %]
[%- HTML.escape(section.fb_number) %]: [%- HTML.escape(section.title) %]
+
+ [%- END %] +
diff --git a/templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html new file mode 100644 index 000000000..a9f6fe554 --- /dev/null +++ b/templates/webpages/requirement_spec/_edit_time_and_cost_estimate_item.html @@ -0,0 +1,28 @@ +[%- USE HTML -%][%- USE LxERP -%][%- USE P -%][%- USE L -%] + + [% L.hidden_tag("requirement_spec_items[+].id", item.id, id = id_prefix _ '_item_id') %] + + + [%- P.simple_format(item.fb_number _ ": " _ item.description) -%] + + + [%- L.select_tag('requirement_spec_items[].complexity_id', SELF.complexities, id=id_prefix _ '_complexity_id', title_key='description', default=item.complexity_id, style="width: 100%") %]
+ + + [%- L.select_tag('requirement_spec_items[].risk_id', SELF.risks, id=id_prefix _ '_risk_id', title_key='description', default=item.risk_id, style="width: 100%") %]
+ + [%- IF !item.children.size -%] + [%- P.man_days_tag('requirement_spec_items[].time_estimation', item, id=id_prefix _ '_time_estimation') %] + [%- ELSE -%] +   + [%- END -%] + + +[%- IF item.children.size -%] + [%- FOREACH child = item.children -%] + [%- INCLUDE 'requirement_spec/_edit_time_and_cost_estimate_item.html' + id_prefix = id_prefix + item = child + level = level + 1 -%] + [%- END -%] +[%- END -%] diff --git a/templates/webpages/requirement_spec/_form.html b/templates/webpages/requirement_spec/_form.html index 69a111f14..fe3ca3137 100644 --- a/templates/webpages/requirement_spec/_form.html +++ b/templates/webpages/requirement_spec/_form.html @@ -29,7 +29,7 @@ [% LxERP.t8("Hourly Rate") %] - [% L.input_tag(form_prefix _ "hourly_rate_as_number", SELF.requirement_spec.hourly_rate_as_number, id=id_prefix _ '_hourly_rate_as_number') %] + [% L.input_tag("requirement_spec.hourly_rate_as_number", SELF.requirement_spec.hourly_rate_as_number, id=id_prefix _ '_hourly_rate_as_number') %] diff --git a/templates/webpages/requirement_spec/_show_time_and_cost_estimate.html b/templates/webpages/requirement_spec/_show_time_and_cost_estimate.html new file mode 100644 index 000000000..1c51e052f --- /dev/null +++ b/templates/webpages/requirement_spec/_show_time_and_cost_estimate.html @@ -0,0 +1,53 @@ +[%- USE LxERP -%][%- USE L -%][%- USE HTML -%][%- USE P -%] +[%- DEFAULT id_prefix = 'time_and_cost_estimate_form' %] + +
+ [%- IF !SELF.requirement_spec.sections.size %] +

[%- LxERP.t8("Neither sections nor function blocks have been created yet.") %]

+ + [%- ELSE %] + + [%- SET at_least_one_function_block = 0 %] + + + + [%- FOREACH section = SELF.requirement_spec.sections %] + + + + + + + + + + + + + [%- IF section.children.size %] + [%- SET at_least_one_function_block = 1 %] + [%- FOREACH child = section.children %] + [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html' + item = child + level = 1 %] + [%- END %] + + + + + + + [%- END -%] + [%- END -%] + + + + + + + + + +
[%- LxERP.t8("Function block") %][%- LxERP.t8("Complexity") %][%- LxERP.t8("Risk") %][%- LxERP.t8("Time estimate") %][%- LxERP.t8("Cost") %]
[%- HTML.escape(section.fb_number) %]: [%- HTML.escape(section.title) %]
[%- LxERP.t8("Sum for section") -%]:[%- P.format_man_days(section.time_estimation, 'skip_zero'=1) -%][%- LxERP.format_amount(section.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR
[%- LxERP.t8("Sum for #1", SELF.requirement_spec.type.description) -%]:[%- P.format_man_days(SELF.requirement_spec.time_estimation) -%][%- LxERP.format_amount(SELF.requirement_spec.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR
+ [%- END %] +
diff --git a/templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html b/templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html new file mode 100644 index 000000000..844afd0b7 --- /dev/null +++ b/templates/webpages/requirement_spec/_show_time_and_cost_estimate_item.html @@ -0,0 +1,29 @@ +[%- USE HTML -%][%- USE LxERP -%][%- USE P -%] + + + [%- P.simple_format(item.fb_number _ ": " _ item.description) -%] + + [%- HTML.escape(item.complexity.description) -%] + [%- HTML.escape(item.risk.description) -%] + [%- IF !item.children.size -%] + [%- P.format_man_days(item.time_estimation, skip_zero=1) -%] + [%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR + [%- ELSE -%] +   +   + [%- END -%] + + +[%- IF item.children.size -%] + [%- FOREACH child = item.children -%] + [%- INCLUDE 'requirement_spec/_show_time_and_cost_estimate_item.html' + item = child + level = level + 1 -%] + [%- END -%] + + + [%- LxERP.t8("Sum for #1", item.fb_number) -%]: + [%- P.format_man_days(item.time_estimation, skip_zero=1) -%] + [%- LxERP.format_amount(item.time_estimation * SELF.requirement_spec.hourly_rate, 2) -%] EUR + +[%- END -%] diff --git a/templates/webpages/requirement_spec/show.html b/templates/webpages/requirement_spec/show.html index f3e75e721..4cae971e5 100644 --- a/templates/webpages/requirement_spec/show.html +++ b/templates/webpages/requirement_spec/show.html @@ -10,6 +10,7 @@