aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile.lock5
-rw-r--r--INSTALL.md8
-rw-r--r--app/assets/stylesheets/components/_forms.sass4
-rw-r--r--app/assets/stylesheets/components/_modals.sass5
-rw-r--r--app/assets/stylesheets/components/_tables.sass10
-rw-r--r--app/controllers/api/v1/imports_controller.rb6
-rw-r--r--app/controllers/compliance_control_sets_controller.rb8
-rw-r--r--app/controllers/compliance_controls_controller.rb2
-rw-r--r--app/decorators/compliance_control_set_decorator.rb7
-rw-r--r--app/javascript/journey_patterns/actions/index.js5
-rw-r--r--app/javascript/journey_patterns/components/EditModal.js58
-rw-r--r--app/javascript/journey_patterns/components/JourneyPattern.js9
-rw-r--r--app/javascript/journey_patterns/containers/Modal.js1
-rw-r--r--app/javascript/vehicle_journeys/actions/index.js9
-rw-r--r--app/javascript/vehicle_journeys/components/Tools.js53
-rw-r--r--app/javascript/vehicle_journeys/components/VehicleJourney.js48
-rw-r--r--app/javascript/vehicle_journeys/components/tools/CreateModal.js6
-rw-r--r--app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js6
-rw-r--r--app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js19
-rw-r--r--app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js38
-rw-r--r--app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js154
-rw-r--r--app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js5
-rw-r--r--app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js94
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js6
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js2
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js5
-rw-r--r--app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js2
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js4
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js6
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js3
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js7
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js7
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js4
-rw-r--r--app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js5
-rw-r--r--app/models/import.rb3
-rw-r--r--app/services/zip_service.rb18
-rw-r--r--app/views/compliance_control_sets/show.html.slim10
-rw-r--r--app/views/compliance_controls/show.html.slim6
-rw-r--r--app/views/imports/_form.html.slim2
-rw-r--r--app/views/stif/dashboards/_dashboard.html.slim8
-rw-r--r--app/workers/workbench_import_worker.rb31
-rw-r--r--config/initializers/relationship.rb17
-rw-r--r--config/locales/compliance_control_sets.en.yml7
-rw-r--r--config/locales/compliance_control_sets.fr.yml7
-rw-r--r--config/locales/import_messages.en.yml3
-rw-r--r--config/locales/import_messages.fr.yml3
-rw-r--r--config/locales/imports.en.yml4
-rw-r--r--config/locales/imports.fr.yml4
-rw-r--r--config/locales/routes.fr.yml2
-rw-r--r--config/routes.rb1
-rw-r--r--lib/compliance_control_set_cloner.rb1
-rw-r--r--lib/stif/dashboard.rb2
-rw-r--r--lib/tasks/erd.rake4
-rw-r--r--package.json2
-rw-r--r--spec/controllers/compliance_controls_controller_spec.rb2
-rw-r--r--spec/factories/imports.rb17
-rw-r--r--spec/factories/netex_imports.rb2
-rw-r--r--spec/factories/workbench_imports.rb2
-rw-r--r--spec/fixtures/OFFRE_WITH_EXTRA.zipbin0 -> 5586 bytes
-rw-r--r--spec/models/import_spec.rb3
-rw-r--r--spec/services/zip_service/regression_4273_spec.rb59
-rw-r--r--spec/services/zip_service_spec.rb68
-rw-r--r--spec/support/random.rb6
-rw-r--r--spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb48
-rw-r--r--spec/workers/workbench_import/workbench_import_worker_spec.rb (renamed from spec/workers/workbench_import_worker_spec.rb)45
65 files changed, 662 insertions, 336 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index ff1e539c9..48a8b638a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -257,7 +257,8 @@ GEM
htmlbeautifier (1.3.1)
httparty (0.14.0)
multi_xml (>= 0.5.2)
- i18n (0.8.6)
+ i18n (0.9.0)
+ concurrent-ruby (~> 1.0)
i18n-tasks (0.9.15)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
@@ -381,7 +382,7 @@ GEM
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
- rails-erd (1.5.0)
+ rails-erd (1.5.2)
activerecord (>= 3.2)
activesupport (>= 3.2)
choice (~> 0.2.0)
diff --git a/INSTALL.md b/INSTALL.md
index bd4a3f330..6e497b580 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -19,6 +19,14 @@ Go into your local repro and install the gems
bundle
+## Node and Yarn
+
+Yarn needs a node version ≥ 6, if you use Node Version Manager [NVM](https://github.com/creationix/nvm) you can rely on the content of `.nvmrc`.
+
+Otherwise please make sure to use a compatible version, still best to use the same as indicated by `.nvrmc`.
+
+Then install yarn (`brew install yarn` does nicely on macOS).
+
### Installation Caveats
#### Node Related Issue, libv8
diff --git a/app/assets/stylesheets/components/_forms.sass b/app/assets/stylesheets/components/_forms.sass
index 7a5323011..2b715d669 100644
--- a/app/assets/stylesheets/components/_forms.sass
+++ b/app/assets/stylesheets/components/_forms.sass
@@ -53,6 +53,8 @@ input
border-right: none
&:last-child
border-left: none
+ &[readonly]
+ background-color: white
+ span
display: table-cell
@@ -61,7 +63,7 @@ input
border-bottom: 1px solid #ccc
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075)
- &.disabled > .form-control + span
+ &[disabled], &.disabled > .form-control + span
background-color: #eee
// Validations
diff --git a/app/assets/stylesheets/components/_modals.sass b/app/assets/stylesheets/components/_modals.sass
index 2db4fe955..e52a2e125 100644
--- a/app/assets/stylesheets/components/_modals.sass
+++ b/app/assets/stylesheets/components/_modals.sass
@@ -38,6 +38,11 @@ $modalW: 600px
.modal-title
font-size: $h2-size
+ display: inline-block
+
+ .modal-close
+ text-align: right
+ display: inline-block
.modal-body
padding: 15px 30px
diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass
index 8fe7be374..178ec2f36 100644
--- a/app/assets/stylesheets/components/_tables.sass
+++ b/app/assets/stylesheets/components/_tables.sass
@@ -247,6 +247,16 @@
width: 35px
height: 35px
margin: 5px
+ &.with_text
+ width: initial
+ > a, > button
+ border-radius: 4%
+ text-decoration: none
+ span
+ &.fa
+ padding-left: 10px
+ span
+ padding: 0 10px 0 0
> a, > button
display: block
diff --git a/app/controllers/api/v1/imports_controller.rb b/app/controllers/api/v1/imports_controller.rb
index 6050418d8..3d7f4ca79 100644
--- a/app/controllers/api/v1/imports_controller.rb
+++ b/app/controllers/api/v1/imports_controller.rb
@@ -5,7 +5,11 @@ class Api::V1::ImportsController < Api::V1::IbooController
def create
args = workbench_import_params.merge(creator: 'Webservice')
@import = parent.workbench_imports.create(args)
- create!
+ if @import.valid?
+ create!
+ else
+ render json: { status: "error", messages: @import.errors.full_messages }
+ end
end
private
diff --git a/app/controllers/compliance_control_sets_controller.rb b/app/controllers/compliance_control_sets_controller.rb
index a1c4f19f0..570204065 100644
--- a/app/controllers/compliance_control_sets_controller.rb
+++ b/app/controllers/compliance_control_sets_controller.rb
@@ -23,6 +23,12 @@ class ComplianceControlSetsController < InheritedResources::Base
end
end
+ def clone
+ ComplianceControlSetCloner.new.copy(params[:id], current_organisation.id)
+ flash[:notice] = I18n.t("compliance_control_sets.errors.operation_in_progress")
+ redirect_to(compliance_control_sets_path)
+ end
+
protected
def begin_of_association_chain
@@ -48,4 +54,4 @@ class ComplianceControlSetsController < InheritedResources::Base
def compliance_control_set_params
params.require(:compliance_control_set).permit(:name, :id)
end
-end
+end \ No newline at end of file
diff --git a/app/controllers/compliance_controls_controller.rb b/app/controllers/compliance_controls_controller.rb
index 33eb9cc97..bd4a33ff4 100644
--- a/app/controllers/compliance_controls_controller.rb
+++ b/app/controllers/compliance_controls_controller.rb
@@ -1,6 +1,7 @@
class ComplianceControlsController < InheritedResources::Base
defaults resource_class: ComplianceControl
belongs_to :compliance_control_set
+ actions :all, :except => [:show, :index]
def select_type
@sti_subclasses = ComplianceControl.subclasses
@@ -15,7 +16,6 @@ class ComplianceControlsController < InheritedResources::Base
end
def create
- puts build_resource.inspect
create! do |success, failure|
success.html { redirect_to compliance_control_set_path(parent) }
failure.html { render( :action => 'new' ) }
diff --git a/app/decorators/compliance_control_set_decorator.rb b/app/decorators/compliance_control_set_decorator.rb
index f4aa607e1..7515316ce 100644
--- a/app/decorators/compliance_control_set_decorator.rb
+++ b/app/decorators/compliance_control_set_decorator.rb
@@ -4,6 +4,13 @@ class ComplianceControlSetDecorator < Draper::Decorator
def action_links
links = []
+ # if policy.clone?
+ links << Link.new(
+ content: h.t('actions.clone'),
+ href: h.clone_compliance_control_set_path(object.id)
+ )
+ # end
+
# if h.policy(object).destroy?
links << Link.new(
content: h.destroy_link_content,
diff --git a/app/javascript/journey_patterns/actions/index.js b/app/javascript/journey_patterns/actions/index.js
index 0c1cb5f5c..8bea5a990 100644
--- a/app/javascript/journey_patterns/actions/index.js
+++ b/app/javascript/journey_patterns/actions/index.js
@@ -90,7 +90,10 @@ const actions = {
resetValidation: (target) => {
$(target).parent().removeClass('has-error').children('.help-block').remove()
},
- humanOID : (oid) => oid.split(':')[2].split("-").pop(),
+ humanOID : (oid) => {
+ let shortOId = oid.split(':')[2].split("-").pop()
+ return shortOId.length > 10 ? `${shortOId.slice(0, 10)}...` : shortOId
+ },
validateFields : (fields) => {
const test = []
diff --git a/app/javascript/journey_patterns/components/EditModal.js b/app/javascript/journey_patterns/components/EditModal.js
index 699f89b85..e7ce24aa1 100644
--- a/app/javascript/journey_patterns/components/EditModal.js
+++ b/app/javascript/journey_patterns/components/EditModal.js
@@ -13,6 +13,19 @@ export default class EditModal extends Component {
}
}
+ renderModalTitle() {
+ if (this.props.editMode) {
+ return (
+ <h4 className='modal-title'>
+ Editer la mission
+ {this.props.modal.type == 'edit' && <em> "{this.props.modal.modalProps.journeyPattern.name}"</em>}
+ </h4>
+ )
+ } else {
+ return <h4 className='modal-title'> Informations </h4>
+ }
+ }
+
render() {
return (
<div className={ 'modal fade ' + ((this.props.modal.type == 'edit') ? 'in' : '') } id='JourneyPatternModal'>
@@ -20,12 +33,8 @@ export default class EditModal extends Component {
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
- <h4 className='modal-title'>
- Editer la mission
- {(this.props.modal.type == 'edit') && (
- <em> "{this.props.modal.modalProps.journeyPattern.name}"</em>
- )}
- </h4>
+ {this.renderModalTitle()}
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'edit') && (
@@ -37,6 +46,7 @@ export default class EditModal extends Component {
type='text'
ref='name'
className='form-control'
+ disabled={!this.props.editMode}
id={this.props.modal.modalProps.index}
defaultValue={this.props.modal.modalProps.journeyPattern.name}
onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
@@ -52,6 +62,7 @@ export default class EditModal extends Component {
type='text'
ref='published_name'
className='form-control'
+ disabled={!this.props.editMode}
id={this.props.modal.modalProps.index}
defaultValue={this.props.modal.modalProps.journeyPattern.published_name}
onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
@@ -66,6 +77,7 @@ export default class EditModal extends Component {
type='text'
ref='registration_number'
className='form-control'
+ disabled={!this.props.editMode}
id={this.props.modal.modalProps.index}
defaultValue={this.props.modal.modalProps.journeyPattern.registration_number}
onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
@@ -74,24 +86,26 @@ export default class EditModal extends Component {
</div>
</div>
</div>
-
- <div className='modal-footer'>
- <button
- className='btn btn-link'
- data-dismiss='modal'
- type='button'
- onClick={this.props.onModalClose}
+ {
+ this.props.editMode &&
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
>
- Annuler
- </button>
- <button
- className='btn btn-primary'
- type='button'
- onClick={this.handleSubmit.bind(this)}
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
>
- Valider
- </button>
- </div>
+ Valider
+ </button>
+ </div>
+ }
</form>
)}
</div>
diff --git a/app/javascript/journey_patterns/components/JourneyPattern.js b/app/javascript/journey_patterns/components/JourneyPattern.js
index dde73a957..34d102c5d 100644
--- a/app/javascript/journey_patterns/components/JourneyPattern.js
+++ b/app/javascript/journey_patterns/components/JourneyPattern.js
@@ -56,7 +56,7 @@ export default class JourneyPattern extends Component{
}
isDisabled(action) {
- return !this.props.status.policy[`journey_patterns.${action}`] && !this.props.editMode
+ return !this.props.status.policy[`journey_patterns.${action}`]
}
render() {
@@ -88,16 +88,17 @@ export default class JourneyPattern extends Component{
data-toggle='modal'
data-target='#JourneyPatternModal'
>
- Editer
+ {this.props.editMode ? 'Editer' : 'Consulter'}
</button>
</li>
<li className={this.props.value.object_id ? '' : 'disabled'}>
{this.vehicleJourneyURL(this.props.value.object_id)}
</li>
- <li className={'delete-action' + (this.isDisabled('destroy') ? ' disabled' : '')}>
+ <li className={'delete-action' + (this.isDisabled('destroy') || !this.props.editMode ? ' disabled' : '')}>
<button
type='button'
- disabled={this.isDisabled('destroy') ? 'disabled' : ''}
+ className="disabled"
+ disabled={this.isDisabled('destroy') || !this.props.editMode}
onClick={(e) => {
e.preventDefault()
this.props.onDeleteJourneyPattern(this.props.index)}
diff --git a/app/javascript/journey_patterns/containers/Modal.js b/app/javascript/journey_patterns/containers/Modal.js
index ace71a857..33ee8583c 100644
--- a/app/javascript/journey_patterns/containers/Modal.js
+++ b/app/javascript/journey_patterns/containers/Modal.js
@@ -5,6 +5,7 @@ import CreateModal from '../components/CreateModal'
const mapStateToProps = (state) => {
return {
+ editMode: state.editMode,
modal: state.modal,
journeyPattern: state.journeyPattern
}
diff --git a/app/javascript/vehicle_journeys/actions/index.js b/app/javascript/vehicle_journeys/actions/index.js
index 4272c7915..95c739893 100644
--- a/app/javascript/vehicle_journeys/actions/index.js
+++ b/app/javascript/vehicle_journeys/actions/index.js
@@ -269,7 +269,10 @@ const actions = {
type: 'RECEIVE_TOTAL_COUNT',
total
}),
- humanOID: (oid) => oid.split(':')[2].split("-").pop(),
+ humanOID: (oid) => {
+ let shortOId = oid.split(':')[2].split("-").pop()
+ return shortOId.length > 10 ? `${shortOId.slice(0, 10)}...` : shortOId
+ },
fetchVehicleJourneys : (dispatch, currentPage, nextPage, queryString) => {
if(currentPage == undefined){
currentPage = 1
@@ -458,6 +461,10 @@ const actions = {
}
}
},
+ escapeWildcardCharacters(search) {
+ let newSearch = search.replace(/^_/, "\\_")
+ return newSearch.replace(/^%/, "\\%")
+ }
}
export default actions
diff --git a/app/javascript/vehicle_journeys/components/Tools.js b/app/javascript/vehicle_journeys/components/Tools.js
index a717408b9..7621dfc10 100644
--- a/app/javascript/vehicle_journeys/components/Tools.js
+++ b/app/javascript/vehicle_journeys/components/Tools.js
@@ -1,4 +1,4 @@
-import React, { PropTypes } from 'react'
+import React, { PropTypes, Component } from 'react'
import actions from '../actions'
import AddVehicleJourney from '../containers/tools/AddVehicleJourney'
import DeleteVehicleJourneys from '../containers/tools/DeleteVehicleJourneys'
@@ -8,28 +8,37 @@ import EditVehicleJourney from '../containers/tools/EditVehicleJourney'
import NotesEditVehicleJourney from '../containers/tools/NotesEditVehicleJourney'
import TimetablesEditVehicleJourney from '../containers/tools/TimetablesEditVehicleJourney'
-export default function Tools({vehicleJourneys, onCancelSelection, filters: {policy}, editMode}) {
- return (
- <div>
- {
- (policy['vehicle_journeys.create'] && policy['vehicle_journeys.update'] && policy['vehicle_journeys.destroy'] && editMode) &&
- <div className='select_toolbox'>
- <ul>
- <AddVehicleJourney />
- <DuplicateVehicleJourney />
- <ShiftVehicleJourney />
- <EditVehicleJourney />
- <TimetablesEditVehicleJourney />
- <NotesEditVehicleJourney />
- <DeleteVehicleJourneys />
- </ul>
- <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span>
- <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>Annuler la sélection</button>
- </div>
- }
- </div>
- )
+export default class Tools extends Component {
+ constructor(props) {
+ super(props)
+ this.hasPolicy = this.hasPolicy.bind(this)
+ }
+
+ hasPolicy(key) {
+ // Check if the user has the policy to disable or not the action
+ return this.props.filters.policy[`vehicle_journeys.${key}`]
+ }
+
+ render() {
+ let { vehicleJourneys, onCancelSelection, editMode } = this.props
+ return (
+ <div className='select_toolbox'>
+ <ul>
+ <AddVehicleJourney disabled={this.hasPolicy("create") && !editMode} />
+ <DuplicateVehicleJourney disabled={this.hasPolicy("create") && this.hasPolicy("update") && !editMode}/>
+ <ShiftVehicleJourney disabled={this.hasPolicy("update") && !editMode}/>
+ <EditVehicleJourney disabled={!this.hasPolicy("update")}/>
+ <TimetablesEditVehicleJourney disabled={!this.hasPolicy("update")}/>
+ <NotesEditVehicleJourney disabled={!this.hasPolicy("update")}/>
+ <DeleteVehicleJourneys disabled={this.hasPolicy("destroy") && !editMode}/>
+ </ul>
+
+ <span className='info-msg'>{actions.getSelected(vehicleJourneys).length} course(s) sélectionnée(s)</span>
+ <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>Annuler la sélection</button>
+ </div>
+ )
+ }
}
Tools.propTypes = {
diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js
index cb5407f81..13f8eced2 100644
--- a/app/javascript/vehicle_journeys/components/VehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js
@@ -44,6 +44,7 @@ export default class VehicleJourney extends Component {
render() {
this.previousCity = undefined
+ let {time_tables} = this.props.value
return (
<div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.errors ? ' has-error': '')}>
@@ -51,39 +52,37 @@ export default class VehicleJourney extends Component {
<div className='strong mb-xs'>{this.props.value.objectid ? actions.humanOID(this.props.value.objectid) : '-'}</div>
<div>{actions.humanOID(this.props.value.journey_pattern.objectid)}</div>
<div>
- {this.props.value.time_tables.map((tt, i)=>
+ {time_tables.slice(0,3).map((tt, i)=>
<span key={i} className='vj_tt'>{this.timeTableURL(tt)}</span>
)}
+ {time_tables.length > 3 && <span className='vj_tt'> + {time_tables.length - 3}</span>}
+ </div>
+ <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}>
+ <input
+ id={this.props.index}
+ name={this.props.index}
+ style={{display: 'none'}}
+ onChange={(e) => this.props.onSelectVehicleJourney(this.props.index)}
+ type='checkbox'
+ disabled={this.props.value.deletable}
+ checked={this.props.value.selected}
+ ></input>
+ <label htmlFor={this.props.index}></label>
</div>
-
- {(this.props.filters.policy['vehicle_journeys.update'] == true && this.props.editMode) &&
- <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}>
- <input
- id={this.props.index}
- name={this.props.index}
- style={{display: 'none'}}
- onChange={(e) => this.props.onSelectVehicleJourney(this.props.index)}
- type='checkbox'
- disabled={this.props.value.deletable}
- checked={this.props.value.selected}
- ></input>
- <label htmlFor={this.props.index}></label>
- </div>
- }
-
</div>
{this.props.value.vehicle_journey_at_stops.map((vj, i) =>
<div key={i} className='td text-center'>
<div className={'cellwrap' + (this.cityNameChecker(vj) ? ' headlined' : '')}>
{this.props.filters.toggleArrivals &&
<div data-headline='Arrivée à'>
- <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}>
+ <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false) ? 'disabled ' : '') + 'input-group time'}>
<input
type='number'
min='00'
max='23'
className='form-control'
- disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}
+ readOnly={!this.props.editMode && !vj.dummy}
onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', false, false)}}
value={vj.arrival_time['hour']}
/>
@@ -93,7 +92,8 @@ export default class VehicleJourney extends Component {
min='00'
max='59'
className='form-control'
- disabled={((this.isDisabled(this.props.value.deletable), vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}
+ readOnly={!this.props.editMode && !vj.dummy}
onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'minute', false, false)}}
value={vj.arrival_time['minute']}
/>
@@ -106,13 +106,14 @@ export default class VehicleJourney extends Component {
}
</div>
<div data-headline='Départ à'>
- <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false) ? 'disabled ' : '') + 'input-group time'}>
+ <span className={((this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false) ? 'disabled ' : '') + 'input-group time'}>
<input
type='number'
min='00'
max='23'
className='form-control'
- disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}
+ readOnly={!this.props.editMode && !vj.dummy}
onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, 'hour', true, this.props.filters.toggleArrivals)}}
value={vj.departure_time['hour']}
/>
@@ -122,7 +123,8 @@ export default class VehicleJourney extends Component {
min='00'
max='59'
className='form-control'
- disabled={(this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false || this.props.editMode == false)}
+ disabled={this.isDisabled(this.props.value.deletable, vj.dummy) || this.props.filters.policy['vehicle_journeys.update'] == false}
+ readOnly={!this.props.editMode && !vj.dummy}
onChange={(e) => {this.props.onUpdateTime(e, i, this.props.index, "minute", true, this.props.filters.toggleArrivals)}}
value={vj.departure_time['minute']}
/>
diff --git a/app/javascript/vehicle_journeys/components/tools/CreateModal.js b/app/javascript/vehicle_journeys/components/tools/CreateModal.js
index 5b5e2f849..2bffebdf6 100644
--- a/app/javascript/vehicle_journeys/components/tools/CreateModal.js
+++ b/app/javascript/vehicle_journeys/components/tools/CreateModal.js
@@ -25,7 +25,7 @@ export default class CreateModal extends Component {
<li className='st_action'>
<button
type='button'
- disabled={((this.props.filters.policy['vehicle_journeys.update'] == true) ? '' : 'disabled')}
+ disabled={(this.props.disabled) }
data-toggle='modal'
data-target='#NewVehicleJourneyModal'
onClick={this.props.onOpenCreateModal}
@@ -39,6 +39,7 @@ export default class CreateModal extends Component {
<div className='modal-content'>
<div className='modal-header'>
<h4 className='modal-title'>Ajouter une course</h4>
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'create') && (
@@ -127,5 +128,6 @@ CreateModal.propTypes = {
onOpenCreateModal: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onAddVehicleJourney: PropTypes.func.isRequired,
- onSelect2JourneyPattern: PropTypes.func.isRequired
+ onSelect2JourneyPattern: PropTypes.func.isRequired,
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js
index 0a1dedd3c..fc13ae964 100644
--- a/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js
+++ b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js
@@ -1,12 +1,12 @@
import React, { PropTypes } from 'react'
import actions from '../../actions'
-export default function DeleteVehicleJourneys({onDeleteVehicleJourneys, vehicleJourneys, filters}) {
+export default function DeleteVehicleJourneys({onDeleteVehicleJourneys, vehicleJourneys, disabled}) {
return (
<li className='st_action'>
<button
type='button'
- disabled={(actions.getSelected(vehicleJourneys).length > 0 && filters.policy['vehicle_journeys.destroy']) ? '' : 'disabled'}
+ disabled={(actions.getSelected(vehicleJourneys).length == 0 || disabled)}
onClick={e => {
e.preventDefault()
onDeleteVehicleJourneys()
@@ -22,5 +22,5 @@ export default function DeleteVehicleJourneys({onDeleteVehicleJourneys, vehicleJ
DeleteVehicleJourneys.propTypes = {
onDeleteVehicleJourneys: PropTypes.func.isRequired,
vehicleJourneys: PropTypes.array.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js
index 0c1c81114..8083defb9 100644
--- a/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js
@@ -8,6 +8,7 @@ export default class DuplicateVehicleJourney extends Component {
this.state = {}
this.onFormChange = this.onFormChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
+ this.disableValidateButton = this.disableValidateButton.bind(this)
}
componentWillReceiveProps() {
@@ -58,16 +59,26 @@ export default class DuplicateVehicleJourney extends Component {
return vjas.departure_time[type]
}
+ disableValidateButton() {
+ /* We disable the button in two cases :
+ - if the additional_time_hh or additional_time_mm are above their input max value
+ - if if their is no change in the other inputs to avoid making a coping of the selected VJ
+ */
+ let incorrectDT = isNaN(this.state.duplicate_time_hh) || isNaN(this.state.duplicate_time_mm) || this.state.duplicate_time_hh > 23 || this.state.duplicate_time_mm > 59
+ let noInputChanges = this.state.additional_time == 0 && this.state.originalDT.hour == this.state.duplicate_time_hh && this.state.originalDT.minute == this.state.duplicate_time_mm
+ return incorrectDT || noInputChanges
+ }
+
render() {
if(this.props.status.isFetching == true) {
return false
}
- if(this.props.status.fetchSuccess == true && actions.getSelected(this.props.vehicleJourneys).length > 0) {
+ if(this.props.status.fetchSuccess == true) {
return (
<li className='st_action'>
<button
type='button'
- disabled={((actions.getSelected(this.props.vehicleJourneys).length >= 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled')}
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length == 0 || this.props.disabled)}
data-toggle='modal'
data-target='#DuplicateVehicleJourneyModal'
onClick={this.props.onOpenDuplicateModal}
@@ -83,6 +94,7 @@ export default class DuplicateVehicleJourney extends Component {
<h4 className='modal-title'>
Dupliquer { actions.getSelected(this.props.vehicleJourneys).length > 1 ? 'plusieurs courses' : 'une course' }
</h4>
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'duplicate') && (
@@ -171,6 +183,7 @@ export default class DuplicateVehicleJourney extends Component {
className={'btn btn-primary ' + (this.state.additional_time == 0 && this.state.originalDT.hour == this.state.duplicate_time_hh && this.state.originalDT.minute == this.state.duplicate_time_mm ? 'disabled' : '')}
type='button'
onClick={this.handleSubmit}
+ disabled={this.disableValidateButton()}
>
Valider
</button>
@@ -192,5 +205,5 @@ export default class DuplicateVehicleJourney extends Component {
DuplicateVehicleJourney.propTypes = {
onOpenDuplicateModal: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js
index 3a4a57024..7ad3cf510 100644
--- a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js
@@ -32,7 +32,7 @@ export default class EditVehicleJourney extends Component {
<li className='st_action'>
<button
type='button'
- disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length != 1 || this.props.disabled)}
data-toggle='modal'
data-target='#EditVehicleJourneyModal'
onClick={() => this.props.onOpenEditModal(actions.getSelected(this.props.vehicleJourneys)[0])}
@@ -46,6 +46,7 @@ export default class EditVehicleJourney extends Component {
<div className='modal-content'>
<div className='modal-header'>
<h4 className='modal-title'>Informations</h4>
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'edit') && (
@@ -59,6 +60,7 @@ export default class EditVehicleJourney extends Component {
type='text'
ref='published_journey_name'
className='form-control'
+ disabled={!this.props.editMode}
defaultValue={this.props.modal.modalProps.vehicleJourney.published_journey_name}
onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
/>
@@ -85,6 +87,7 @@ export default class EditVehicleJourney extends Component {
type='text'
ref='published_journey_identifier'
className='form-control'
+ disabled={!this.props.editMode}
defaultValue={this.props.modal.modalProps.vehicleJourney.published_journey_identifier}
onKeyDown={(e) => actions.resetValidation(e.currentTarget)}
/>
@@ -94,6 +97,7 @@ export default class EditVehicleJourney extends Component {
<div className='form-group'>
<label className='control-label'>Transporteur</label>
<CompanySelect2
+ editMode={this.props.editMode}
company = {this.props.modal.modalProps.vehicleJourney.company}
onSelect2Company = {(e) => this.props.onSelect2Company(e)}
onUnselect2Company = {() => this.props.onUnselect2Company()}
@@ -127,24 +131,26 @@ export default class EditVehicleJourney extends Component {
</div>
</div>
</div>
-
- <div className='modal-footer'>
- <button
- className='btn btn-link'
- data-dismiss='modal'
- type='button'
- onClick={this.props.onModalClose}
+ {
+ this.props.editMode &&
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
>
- Annuler
+ Annuler
</button>
- <button
- className='btn btn-primary'
- type='button'
- onClick={this.handleSubmit.bind(this)}
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
>
- Valider
+ Valider
</button>
- </div>
+ </div>
+ }
</form>
)}
@@ -163,5 +169,5 @@ export default class EditVehicleJourney extends Component {
EditVehicleJourney.propTypes = {
onOpenEditModal: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js
index 1958faf5f..de97bc403 100644
--- a/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js
@@ -13,21 +13,25 @@ export default class NotesEditVehicleJourney extends Component {
$('#NotesEditVehicleJourneyModal').modal('hide')
}
- renderFootnoteButton(lf, vjArray){
- let footnote_id = undefined
- vjArray.forEach((f) => {
- if(f.id == lf.id){
- footnote_id = f.id
- }
- })
+ footnotes() {
+ let { footnotes } = this.props.modal.modalProps.vehicleJourney
+ let fnIds = footnotes.map(fn => fn.id)
+ return {
+ associated: footnotes,
+ to_associate: window.line_footnotes.filter(fn => !fnIds.includes(fn.id))
+ }
+ }
+
+ renderFootnoteButton(lf) {
+ if (!this.props.editMode) return false
- if(footnote_id){
+ if (this.footnotes().associated.includes(lf)) {
return <button
type='button'
className='btn btn-outline-danger btn-xs'
onClick={() => this.props.onToggleFootnoteModal(lf, false)}
><span className="fa fa-trash"></span> Retirer</button>
- }else{
+ } else {
return <button
type='button'
className='btn btn-outline-primary btn-xs'
@@ -36,28 +40,64 @@ export default class NotesEditVehicleJourney extends Component {
}
}
- filterFN() {
- return _.filter(window.line_footnotes, (lf, i) => {
- let bool = true
- _.map(this.props.modal.modalProps.vehicleJourney.footnotes, (f, j) => {
- if(lf.id === f.id) {
- bool = false
- }
- })
- return bool
- })
+ renderAssociatedFN() {
+ if (this.footnotes().associated.length == 0) {
+ return <h3>Aucune note associée</h3>
+ } else {
+ return (
+ <div>
+ <h3>Notes associées :</h3>
+ {this.footnotes().associated.map((lf, i) =>
+ <div
+ key={i}
+ className='panel panel-default'
+ >
+ <div className='panel-heading'>
+ <h4 className='panel-title clearfix'>
+ <div className='pull-left' style={{ paddingTop: '3px' }}>{lf.code}</div>
+ <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div>
+ </h4>
+ </div>
+ <div className='panel-body'><p>{lf.label}</p></div>
+ </div>
+ )}
+ </div>
+ )
+ }
+ }
+
+ renderToAssociateFN() {
+ if (window.line_footnotes.length == 0) return <h3>La ligne ne possède pas de notes</h3>
+
+ if (this.footnotes().to_associate.length == 0) return false
+
+ return (
+ <div>
+ <h3 className='mt-lg'>Sélectionnez les notes à associer à cette course :</h3>
+ {this.footnotes().to_associate.map((lf, i) =>
+ <div key={i} className='panel panel-default'>
+ <div className='panel-heading'>
+ <h4 className='panel-title clearfix'>
+ <div className='pull-left' style={{ paddingTop: '3px' }}>{lf.code}</div>
+ <div className='pull-right'>{this.renderFootnoteButton(lf)}</div>
+ </h4>
+ </div>
+ <div className='panel-body'><p>{lf.label}</p></div>
+ </div>
+ )}
+ </div>
+ )
}
render() {
- if(this.props.status.isFetching == true) {
- return false
- }
- if(this.props.status.fetchSuccess == true) {
+ if (this.props.status.isFetching == true) return false
+
+ if (this.props.status.fetchSuccess == true) {
return (
<li className='st_action'>
<button
type='button'
- disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length != 1 || this.props.disabled)}
data-toggle='modal'
data-target='#NotesEditVehicleJourneyModal'
onClick={() => this.props.onOpenNotesEditModal(actions.getSelected(this.props.vehicleJourneys)[0])}
@@ -71,61 +111,35 @@ export default class NotesEditVehicleJourney extends Component {
<div className='modal-content'>
<div className='modal-header'>
<h4 className='modal-title'>Notes</h4>
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'notes_edit') && (
<form>
<div className='modal-body'>
- <h3>Notes associées</h3>
- {(this.props.modal.modalProps.vehicleJourney.footnotes).map((lf, i) =>
- <div
- key={i}
- className='panel panel-default'
- >
- <div className='panel-heading'>
- <h4 className='panel-title clearfix'>
- <div className='pull-left' style={{paddingTop: '3px'}}>{lf.code}</div>
- <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div>
- </h4>
- </div>
- <div className='panel-body'><p>{lf.label}</p></div>
- </div>
- )}
-
- <h3 className='mt-lg'>Sélectionnez les notes à associer à cette course :</h3>
- {this.filterFN().map((lf, i) =>
- <div
- key={i}
- className='panel panel-default'
- >
- <div className='panel-heading'>
- <h4 className='panel-title clearfix'>
- <div className='pull-left' style={{paddingTop: '3px'}}>{lf.code}</div>
- <div className='pull-right'>{this.renderFootnoteButton(lf, this.props.modal.modalProps.vehicleJourney.footnotes)}</div>
- </h4>
- </div>
- <div className='panel-body'><p>{lf.label}</p></div>
- </div>
- )}
+ {this.renderAssociatedFN()}
+ {this.props.editMode && this.renderToAssociateFN()}
</div>
-
- <div className='modal-footer'>
- <button
- className='btn btn-link'
- data-dismiss='modal'
- type='button'
- onClick={this.props.onModalClose}
+ {
+ this.props.editMode &&
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
>
- Annuler
+ Annuler
</button>
- <button
- className='btn btn-primary'
- type='button'
- onClick={this.handleSubmit.bind(this)}
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit.bind(this)}
>
- Valider
+ Valider
</button>
- </div>
+ </div>
+ }
</form>
)}
@@ -146,5 +160,5 @@ NotesEditVehicleJourney.propTypes = {
onModalClose: PropTypes.func.isRequired,
onToggleFootnoteModal: PropTypes.func.isRequired,
onNotesEditVehicleJourney: PropTypes.func.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js
index c1e2de779..175106ac5 100644
--- a/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js
@@ -34,7 +34,7 @@ export default class ShiftVehicleJourney extends Component {
<li className='st_action'>
<button
type='button'
- disabled={(actions.getSelected(this.props.vehicleJourneys).length == 1 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length > 1 || this.props.disabled)}
data-toggle='modal'
data-target='#ShiftVehicleJourneyModal'
onClick={this.props.onOpenShiftModal}
@@ -51,6 +51,7 @@ export default class ShiftVehicleJourney extends Component {
{(this.props.modal.type == 'shift') && (
<em>Mettre à jour les horaires de la course {actions.humanOID(actions.getSelected(this.props.vehicleJourneys)[0].objectid)}</em>
)}
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'shift') && (
@@ -110,5 +111,5 @@ export default class ShiftVehicleJourney extends Component {
ShiftVehicleJourney.propTypes = {
onOpenShiftModal: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js
index fd2304901..fef3cdcc9 100644
--- a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js
@@ -5,6 +5,8 @@ import TimetableSelect2 from './select2s/TimetableSelect2'
export default class TimetablesEditVehicleJourney extends Component {
constructor(props) {
super(props)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ this.timeTableURL = this.timeTableURL.bind(this)
}
handleSubmit() {
@@ -13,6 +15,11 @@ export default class TimetablesEditVehicleJourney extends Component {
$('#CalendarsEditVehicleJourneyModal').modal('hide')
}
+ timeTableURL(tt) {
+ let refURL = window.location.pathname.split('/', 3).join('/')
+ return refURL + '/time_tables/' + tt.id
+ }
+
render() {
if(this.props.status.isFetching == true) {
return false
@@ -22,7 +29,7 @@ export default class TimetablesEditVehicleJourney extends Component {
<li className='st_action'>
<button
type='button'
- disabled={(actions.getSelected(this.props.vehicleJourneys).length > 0 && this.props.filters.policy['vehicle_journeys.update']) ? '' : 'disabled'}
+ disabled={(actions.getSelected(this.props.vehicleJourneys).length != 1 || this.props.disabled)}
data-toggle='modal'
data-target='#CalendarsEditVehicleJourneyModal'
onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))}
@@ -36,6 +43,7 @@ export default class TimetablesEditVehicleJourney extends Component {
<div className='modal-content'>
<div className='modal-header'>
<h4 className='modal-title'>Calendriers associés</h4>
+ <span type="button" className="close modal-close" data-dismiss="modal">&times;</span>
</div>
{(this.props.modal.type == 'calendars_edit') && (
@@ -57,55 +65,63 @@ export default class TimetablesEditVehicleJourney extends Component {
{this.props.modal.modalProps.timetables.map((tt, i) =>
<div className='nested-fields' key={i}>
<div className='wrapper'>
- <div>{tt.comment}</div>
- <div>
- <a
- href='#'
- title='Supprimer'
- className='fa fa-trash remove_fields'
- style={{height: 'auto', lineHeight: 'normal'}}
- onClick={(e) => {
- e.preventDefault()
- this.props.onDeleteCalendarModal(tt)
- }}
+ <div> <a href={this.timeTableURL(tt)} target="_blank">{tt.comment}</a> </div>
+ {
+ this.props.editMode &&
+ <div>
+ <a
+ href='#'
+ title='Supprimer'
+ className='fa fa-trash remove_fields'
+ style={{ height: 'auto', lineHeight: 'normal' }}
+ onClick={(e) => {
+ e.preventDefault()
+ this.props.onDeleteCalendarModal(tt)
+ }}
></a>
- </div>
+ </div>
+ }
</div>
</div>
)}
- <div className='nested-fields'>
- <div className='wrapper'>
- <div>
- <TimetableSelect2
- onSelect2Timetable={this.props.onSelect2Timetable}
- chunkURL={'/autocomplete_time_tables.json'}
- isFilter={false}
- />
+ {
+ this.props.editMode &&
+ <div className='nested-fields'>
+ <div className='wrapper'>
+ <div>
+ <TimetableSelect2
+ onSelect2Timetable={this.props.onSelect2Timetable}
+ chunkURL={'/autocomplete_time_tables.json'}
+ isFilter={false}
+ />
+ </div>
</div>
</div>
- </div>
+ }
</div>
</div>
</div>
</div>
-
- <div className='modal-footer'>
- <button
- className='btn btn-link'
- data-dismiss='modal'
- type='button'
- onClick={this.props.onModalClose}
+ {
+ this.props.editMode &&
+ <div className='modal-footer'>
+ <button
+ className='btn btn-link'
+ data-dismiss='modal'
+ type='button'
+ onClick={this.props.onModalClose}
>
- Annuler
- </button>
- <button
- className='btn btn-primary'
- type='button'
- onClick={this.handleSubmit.bind(this)}
+ Annuler
+ </button>
+ <button
+ className='btn btn-primary'
+ type='button'
+ onClick={this.handleSubmit}
>
- Valider
- </button>
- </div>
+ Valider
+ </button>
+ </div>
+ }
</form>
)}
@@ -127,5 +143,5 @@ TimetablesEditVehicleJourney.propTypes = {
onTimetablesEditVehicleJourney: PropTypes.func.isRequired,
onDeleteCalendarModal: PropTypes.func.isRequired,
onSelect2Timetable: PropTypes.func.isRequired,
- filters: PropTypes.object.isRequired
+ disabled: PropTypes.bool.isRequired
} \ No newline at end of file
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js
index 9c259630d..0697e9141 100644
--- a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js
@@ -1,6 +1,7 @@
import _ from 'lodash'
import React, { PropTypes, Component } from 'react'
import Select2 from 'react-select2'
+import actions from '../../../actions'
// get JSON full path
let origin = window.location.origin
@@ -20,10 +21,11 @@ export default class BSelect4 extends Component {
value={(this.props.company) ? this.props.company.name : undefined}
onSelect={(e) => this.props.onSelect2Company(e) }
onUnselect={() => this.props.onUnselect2Company()}
+ disabled={!this.props.editMode}
multiple={false}
ref='company_id'
options={{
- allowClear: true,
+ allowClear: this.props.editMode,
theme: 'bootstrap',
width: '100%',
placeholder: 'Filtrer par transporteur...',
@@ -34,7 +36,7 @@ export default class BSelect4 extends Component {
delay: '500',
data: function(params) {
return {
- q: {name_cont: params.term},
+ q: { name_cont: actions.escapeWildcardCharacters(params.term)},
};
},
processResults: function(data, params) {
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js
index e4abdd651..6069bf089 100644
--- a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js
@@ -33,7 +33,7 @@ export default class BSelect4 extends Component {
delay: '500',
data: function(params) {
return {
- q: {published_name_or_objectid_or_registration_number_cont: params.term},
+ q: { published_name_or_objectid_or_registration_number_cont: actions.escapeWildcardCharacters(params.term)},
};
},
processResults: function(data, params) {
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js
index 606bf8511..60c3eab83 100644
--- a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js
@@ -32,12 +32,9 @@ export default class BSelect4 extends Component {
dataType: 'json',
delay: '500',
data: function(params) {
- let newParmas = params.term.split(" ")
return {
q: {
- objectid_cont_any: newParmas,
- comment_cont_any: newParmas,
- m: 'or'
+ comment_or_objectid_cont_any: actions.escapeWildcardCharacters(params.term)
}
};
},
diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js
index e1af8816d..7cccbbc05 100644
--- a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js
+++ b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js
@@ -33,7 +33,7 @@ export default class BSelect4b extends Component {
delay: '500',
data: function(params) {
return {
- q: {objectid_cont: params.term},
+ q: { objectid_cont: actions.escapeWildcardCharacters(params.term)},
};
},
processResults: function(data, params) {
diff --git a/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js
index b3f777448..5da0bd3e9 100644
--- a/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js
@@ -2,13 +2,13 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import CreateModal from '../../components/tools/CreateModal'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
+ disabled: ownProps.disabled,
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
status: state.status,
stopPointsList: state.stopPointsList,
- filters: state.filters
}
}
diff --git a/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js b/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js
index d7d315da4..95f2eb506 100644
--- a/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js
+++ b/app/javascript/vehicle_journeys/containers/tools/DeleteVehicleJourneys.js
@@ -2,10 +2,10 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import DeleteVJComponent from '../../components/tools/DeleteVehicleJourneys'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
- vehicleJourneys: state.vehicleJourneys,
- filters: state.filters
+ disabled: ownProps.disabled,
+ vehicleJourneys: state.vehicleJourneys
}
}
diff --git a/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js
index e9ca88040..7b23a06dc 100644
--- a/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/DuplicateVehicleJourney.js
@@ -2,8 +2,9 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import DuplicateVJComponent from '../../components/tools/DuplicateVehicleJourney'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
+ disabled: ownProps.disabled,
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
status: state.status,
diff --git a/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js
index 2d480aa0c..c2eabcc10 100644
--- a/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/EditVehicleJourney.js
@@ -2,12 +2,13 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import EditComponent from '../../components/tools/EditVehicleJourney'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
+ editMode: state.editMode,
+ disabled: ownProps.disabled,
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
- status: state.status,
- filters: state.filters
+ status: state.status
}
}
diff --git a/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js
index 5a96ff273..6290ae3bf 100644
--- a/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/NotesEditVehicleJourney.js
@@ -2,12 +2,13 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import NotesEditComponent from '../../components/tools/NotesEditVehicleJourney'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
+ editMode: state.editMode,
+ disabled: ownProps.disabled,
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
- status: state.status,
- filters: state.filters
+ status: state.status
}
}
diff --git a/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js
index a4b4fbe39..abd7dd145 100644
--- a/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/ShiftVehicleJourney.js
@@ -2,12 +2,12 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import ShiftVJComponent from '../../components/tools/ShiftVehicleJourney'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
status: state.status,
- filters: state.filters
+ disabled: ownProps.disabled
}
}
diff --git a/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js
index 62150a06e..b4ba9d068 100644
--- a/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js
+++ b/app/javascript/vehicle_journeys/containers/tools/TimetablesEditVehicleJourney.js
@@ -2,12 +2,13 @@ import actions from '../../actions'
import { connect } from 'react-redux'
import TimetablesEditComponent from '../../components/tools/TimetablesEditVehicleJourney'
-const mapStateToProps = (state) => {
+const mapStateToProps = (state, ownProps) => {
return {
+ editMode: state.editMode,
modal: state.modal,
vehicleJourneys: state.vehicleJourneys,
status: state.status,
- filters: state.filters
+ disabled: ownProps.disabled
}
}
diff --git a/app/models/import.rb b/app/models/import.rb
index 64f713914..e0aae6ef1 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -18,6 +18,7 @@ class Import < ActiveRecord::Base
validates :file, presence: true
validates_presence_of :workbench, :creator
+ validates_format_of :file, with: %r{\.zip\z}i, message: I18n.t('activerecord.errors.models.imports.wrong_file_extension')
before_create :initialize_fields
@@ -34,7 +35,7 @@ class Import < ActiveRecord::Base
end
def self.finished_statuses
- symbols_with_indifferent_access(%i(successful failed aborted canceled))
+ symbols_with_indifferent_access(%i(successful failed warning aborted canceled))
end
def notify_parent
diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb
index cab301b01..7a4bdad1b 100644
--- a/app/services/zip_service.rb
+++ b/app/services/zip_service.rb
@@ -1,10 +1,9 @@
class ZipService
- # TODO: Remove me before merge https://github.com/rubyzip/rubyzip
- class Subdir < Struct.new(:name, :stream)
+ class Subdir < Struct.new(:name, :stream, :spurious)
end
- attr_reader :current_key, :current_output, :yielder
+ attr_reader :current_key, :current_output, :current_spurious, :yielder
def initialize data
@zip_data = StringIO.new(data)
@@ -36,6 +35,7 @@ class ZipService
end
def add_to_current_output entry
+ return if is_spurious! entry.name
current_output.put_next_entry entry.name
write_to_current_output entry.get_input_stream
end
@@ -51,7 +51,8 @@ class ZipService
@yielder << Subdir.new(
current_key,
# Second part of the solution, yield the closed stream
- current_output.close_buffer)
+ current_output.close_buffer,
+ current_spurious)
end
end
@@ -59,10 +60,19 @@ class ZipService
@current_key = entry_key
# First piece of the solution, use internal way to create a Zip::OutputStream
@current_output = Zip::OutputStream.new(StringIO.new(''), true, nil)
+ @current_spurious = []
end
def entry_key entry
# last dir name File.dirname.split("/").last
entry.name.split('/', -1)[-2]
end
+
+ def is_spurious! entry_name
+ segments = entry_name.split('/', 3)
+ return false if segments.size < 3
+
+ current_spurious << segments.second
+ return true
+ end
end
diff --git a/app/views/compliance_control_sets/show.html.slim b/app/views/compliance_control_sets/show.html.slim
index 4bb6b9c77..cf236feb8 100644
--- a/app/views/compliance_control_sets/show.html.slim
+++ b/app/views/compliance_control_sets/show.html.slim
@@ -97,9 +97,13 @@
cls: 'table has-filter has-search'
.select_toolbox
ul
- li.st_action
+ li.st_action.with_text
= link_to select_type_compliance_control_set_compliance_controls_path(@compliance_control_set.id)
span.fa.fa-plus
- li.st_action
+ span
+ = t('compliance_control_sets.actions.add_compliance_control')
+ li.st_action.with_text
= link_to new_compliance_control_set_compliance_control_block_path(@compliance_control_set.id)
- span.fa.fa-plus-square
+ span.fa.fa-plus
+ span
+ = t('compliance_control_sets.actions.add_compliance_control_block')
diff --git a/app/views/compliance_controls/show.html.slim b/app/views/compliance_controls/show.html.slim
index a123d1887..44d52a9f1 100644
--- a/app/views/compliance_controls/show.html.slim
+++ b/app/views/compliance_controls/show.html.slim
@@ -1,9 +1,9 @@
- breadcrumb :compliance_control, @compliance_control
/ PageHeader
-- header_params = ['jeux-de-controle',
+= pageheader 'jeux-de-controle',
t('compliance_controls.show.title'),
- '']
-= pageheader(*header_params) do
+ '',
+ link_to(t('actions.edit'), edit_compliance_control_set_compliance_control_path(params[:compliance_control_set_id], params[:id]), class: 'btn btn-default') do
/ PageContent
.page_content
diff --git a/app/views/imports/_form.html.slim b/app/views/imports/_form.html.slim
index 0fbf578be..95d97c534 100644
--- a/app/views/imports/_form.html.slim
+++ b/app/views/imports/_form.html.slim
@@ -9,6 +9,6 @@
.form-group
= form.label :file, t('activerecord.attributes.import.resources'), class: 'control-label col-sm-4 col-xs-5'
.col-sm-8.col-xs-7
- = form.input_field :file, label: false, class: 'form-control'
+ = form.input :file, label: false, class: 'form-control'
= form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_import_form'
diff --git a/app/views/stif/dashboards/_dashboard.html.slim b/app/views/stif/dashboards/_dashboard.html.slim
index 3142ecd5b..f3cd01f46 100644
--- a/app/views/stif/dashboards/_dashboard.html.slim
+++ b/app/views/stif/dashboards/_dashboard.html.slim
@@ -39,14 +39,14 @@
h3.panel-title.with_actions
div
= t('.referentials')
- span.badge.ml-xs = @referentials.count if @referentials.present?
+ span.badge.ml-xs = @dashboard.referentials.count if @dashboard.referentials.present?
div
= link_to '', workbench_path(@dashboard.workbench), class: ' fa fa-chevron-right pull-right', title: t('.see')
- - if @referentials.present?
+ - if @dashboard.referentials.present?
.list-group
- - @referentials.each_with_index do |referential, i|
+ - @dashboard.referentials.first(5).each_with_index do |referential, i|
= link_to referential.name, referential_path(referential, workbench_id: referential.workbench_id, current_workbench_id: @dashboard.workbench.id), class: 'list-group-item' if i < 6
- else
@@ -65,7 +65,7 @@
- if @dashboard.calendars.present?
.list-group
- - @dashboard.calendars.each_with_index do |calendar, i|
+ - @dashboard.calendars.first(5).each_with_index do |calendar, i|
= link_to calendar.name, calendar_path(calendar), class: 'list-group-item' if i < 6
- else
diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb
index 994493944..300fad9e2 100644
--- a/app/workers/workbench_import_worker.rb
+++ b/app/workers/workbench_import_worker.rb
@@ -14,11 +14,13 @@ class WorkbenchImportWorker
zip_service = ZipService.new(downloaded)
upload zip_service
@workbench_import.update(ended_at: Time.now)
+ rescue Zip::Error
+ handle_corrupt_zip_file
end
def download
logger.info "HTTP GET #{import_url}"
- @zipfile_data = HTTPService.get_resource(
+ HTTPService.get_resource(
host: import_host,
path: import_path,
params: {token: @workbench_import.token_download}).body
@@ -32,6 +34,10 @@ class WorkbenchImportWorker
params: params(eg_file, eg_name))
end
+ def handle_corrupt_zip_file
+ @workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {import_name: @workbench_import.name})
+ end
+
def upload zip_service
entry_group_streams = zip_service.subdirs
@workbench_import.update total_steps: entry_group_streams.size
@@ -42,11 +48,24 @@ class WorkbenchImportWorker
raise
end
- def upload_entry_group entry_pair, element_count
- @workbench_import.update( current_step: element_count.succ )
- # status = retry_service.execute(&upload_entry_group_proc(entry_pair))
- eg_name = entry_pair.name
- eg_stream = entry_pair.stream
+ def update_object_state entry, count
+ @workbench_import.update( current_step: count )
+ unless entry.spurious.empty?
+ @workbench_import.messages.create(
+ criticity: :warning,
+ message_key: 'inconsistent_zip_file',
+ message_attributes: {
+ 'import_name' => @workbench_import.name,
+ 'spurious_dirs' => entry.spurious.join(', ')
+ })
+ end
+ end
+
+ def upload_entry_group entry, element_count
+ update_object_state entry, element_count.succ
+ # status = retry_service.execute(&upload_entry_group_proc(entry))
+ eg_name = entry.name
+ eg_stream = entry.stream
FileUtils.mkdir_p(Rails.root.join('tmp', 'imports'))
diff --git a/config/initializers/relationship.rb b/config/initializers/relationship.rb
new file mode 100644
index 000000000..492aa627f
--- /dev/null
+++ b/config/initializers/relationship.rb
@@ -0,0 +1,17 @@
+if Rails.env.development?
+ require 'rails_erd/domain/relationship'
+
+ module RailsERD
+ class Domain
+ class Relationship
+ class << self
+ private
+
+ def association_identity(association)
+ Set[association_owner(association), association_target(association)]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/locales/compliance_control_sets.en.yml b/config/locales/compliance_control_sets.en.yml
index f72342894..7361edacf 100644
--- a/config/locales/compliance_control_sets.en.yml
+++ b/config/locales/compliance_control_sets.en.yml
@@ -1,7 +1,9 @@
en:
compliance_control_sets:
clone:
- prefix: 'Copie de'
+ prefix: 'Copy of'
+ errors:
+ operation_in_progress: "The clone operation is in progress. Please wait and refresh the page in a few moments"
index:
title: Compliance control set
new: New compliance control set
@@ -13,7 +15,8 @@ en:
edit: Edit
show: Show
destroy: Destroy
- add_compliance_control: Add a compliance control
+ add_compliance_control: Compliance Control
+ add_compliance_control_block: Compliance Control Block
destroy_confirm: Are you sur ?
filters:
name: 'Enter name ...'
diff --git a/config/locales/compliance_control_sets.fr.yml b/config/locales/compliance_control_sets.fr.yml
index c31eb9423..19f6f08ee 100644
--- a/config/locales/compliance_control_sets.fr.yml
+++ b/config/locales/compliance_control_sets.fr.yml
@@ -1,7 +1,9 @@
fr:
compliance_control_sets:
clone:
- prefix: 'Copy of'
+ prefix: 'Copie de'
+ errors:
+ operation_in_progress: "L'opération de clone est en cours. Veuillez patienter et raffraichir la page dans quelques instants"
index:
title: "Liste des jeux de contrôles"
edit:
@@ -15,7 +17,8 @@ fr:
edit: Editer
show: Consulter
destroy: Supprimer
- add_compliance_control: Ajouter un JDC
+ add_compliance_control: Contrôle
+ add_compliance_control_block: Groupe de contrôles
loaded: Charger le contrôle
destroy_confirm: Etes vous sûr de supprimer ce jeux de contrôle ?
filters:
diff --git a/config/locales/import_messages.en.yml b/config/locales/import_messages.en.yml
index 4009d7c77..2048b9794 100644
--- a/config/locales/import_messages.en.yml
+++ b/config/locales/import_messages.en.yml
@@ -1,6 +1,8 @@
en:
import_messages:
compliance_check_messages:
+ corrupt_zip_file: "The zip file of WorkbenchImport %{import_name} is corrupted and cannot be read"
+ inconsistent_zip_file: "The zip file of WorkbenchImport %{import_name} contains the following spurious directories %{spurious_dirs}, which are ignored"
referential_creation: "Le référentiel n'a pas pu être créé car un référentiel existe déjà sur les même périodes et lignes"
1_netexstif_2: "Le fichier %{source_filename} ne respecte pas la syntaxe XML ou la XSD NeTEx : erreur '%{error_value}' rencontré"
1_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a une date de mise à jour dans le futur"
@@ -13,6 +15,7 @@ en:
2_netexstif_3_3: "la frame NETEX_OFFRE_LIGNE du fichier %{source_filename} ne contient pas la frame %{error_value} obligatoire"
2_netexstif_3_4: "la frame NETEX_OFFRE_LIGNE du fichier %{source_filename} contient une frame %{error_value} non acceptée"
2_netexstif_4: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'identifiant %{source_objectid} de l'objet %{error_value} ne respecte pas la syntaxe %{reference_value}"
+ 2_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{error_value} d'identifiant %{source_objectid} a une date de mise à jour dans le futur"
2_netexstif_6: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a un état de modification interdit : 'delete'"
2_netexstif_7: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} définit une référence %{reference_value} de syntaxe invalide : %{error_value}"
2_netexstif_8_1: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} définit une référence %{error_value} de type externe : référence interne attendue"
diff --git a/config/locales/import_messages.fr.yml b/config/locales/import_messages.fr.yml
index 085299bb4..9f0af1faa 100644
--- a/config/locales/import_messages.fr.yml
+++ b/config/locales/import_messages.fr.yml
@@ -1,6 +1,8 @@
fr:
import_messages:
compliance_check_messages:
+ corrupt_zip_file: "Le fichier zip du WorkbenchImport %{import_name} est corrompu, et ne peut être lu"
+ inconsistent_zip_file: "Le fichier zip du WorkbenchImport %{import_name} contient les repertoirs illegeaux %{spurious_dirs} qui seront ignorés"
referential_creation: "Le référentiel n'a pas pu être créé car un référentiel existe déjà sur les même périodes et lignes"
1_netexstif_2: "Le fichier %{source_filename} ne respecte pas la syntaxe XML ou la XSD NeTEx : erreur '%{error_value}' rencontré"
1_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a une date de mise à jour dans le futur"
@@ -13,6 +15,7 @@ fr:
2_netexstif_3_3: "la frame NETEX_OFFRE_LIGNE du fichier %{source_filename} ne contient pas la frame %{error_value} obligatoire"
2_netexstif_3_4: "la frame NETEX_OFFRE_LIGNE du fichier %{source_filename} contient une frame %{error_value} non acceptée"
2_netexstif_4: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'identifiant %{source_objectid} de l'objet %{error_value} ne respecte pas la syntaxe %{reference_value}"
+ 2_netexstif_5: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{error_value} d'identifiant %{source_objectid} a une date de mise à jour dans le futur"
2_netexstif_6: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} a un état de modification interdit : 'delete'"
2_netexstif_7: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} définit une référence %{reference_value} de syntaxe invalide : %{error_value}"
2_netexstif_8_1: "%{source_filename}-Ligne %{source_line_number}-Colonne %{source_column_number} : l'objet %{source_label} d'identifiant %{source_objectid} définit une référence %{error_value} de type externe : référence interne attendue"
diff --git a/config/locales/imports.en.yml b/config/locales/imports.en.yml
index 9bf877c86..f3bcad9e9 100644
--- a/config/locales/imports.en.yml
+++ b/config/locales/imports.en.yml
@@ -53,6 +53,10 @@ en:
zero: "import"
one: "NeTEx import"
other: "imports"
+ errors:
+ models:
+ imports:
+ wrong_file_extension: "The imported file must be a zip file"
attributes:
import:
resources: "File to import"
diff --git a/config/locales/imports.fr.yml b/config/locales/imports.fr.yml
index 6998c89d2..6e74fa33c 100644
--- a/config/locales/imports.fr.yml
+++ b/config/locales/imports.fr.yml
@@ -53,6 +53,10 @@ fr:
zero: "import"
one: "import NeTEx"
other: "imports"
+ errors:
+ models:
+ imports:
+ wrong_file_extension: "Le fichier importé doit être au format zip"
attributes:
import:
resources: "Fichier à importer"
diff --git a/config/locales/routes.fr.yml b/config/locales/routes.fr.yml
index 83a96732d..31838f1a7 100644
--- a/config/locales/routes.fr.yml
+++ b/config/locales/routes.fr.yml
@@ -81,7 +81,7 @@ fr:
number: "Indice"
comment: "Commentaire"
direction: "Direction"
- wayback: "Direction"
+ wayback: "Sens"
stop_points: "Nb arrêts"
journey_patterns: "Nb missions"
opposite_route: "Itinéraire associé"
diff --git a/config/routes.rb b/config/routes.rb
index b9e318f91..b105e77d6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -74,6 +74,7 @@ ChouetteIhm::Application.routes.draw do
resources :api_keys, :only => [:edit, :update, :new, :create, :destroy]
resources :compliance_control_sets do
+ get :clone, on: :member
resources :compliance_controls, except: :index do
get :select_type, on: :collection
end
diff --git a/lib/compliance_control_set_cloner.rb b/lib/compliance_control_set_cloner.rb
index 1cf58a38d..12e1eccb5 100644
--- a/lib/compliance_control_set_cloner.rb
+++ b/lib/compliance_control_set_cloner.rb
@@ -59,7 +59,6 @@ class ComplianceControlSetCloner
criticity: compliance_control.criticity,
name: name_of_copy(:compliance_controls, compliance_control.name),
origin_code: compliance_control.origin_code,
- target: compliance_control.target,
type: compliance_control.type
).tap do | control |
control_id_map.update compliance_control.id => control
diff --git a/lib/stif/dashboard.rb b/lib/stif/dashboard.rb
index fafddec62..b6b6b8284 100644
--- a/lib/stif/dashboard.rb
+++ b/lib/stif/dashboard.rb
@@ -5,7 +5,7 @@ module Stif
end
def referentials
- @referentials ||= @workbench.all_referentials
+ @referentials ||= self.workbench.all_referentials
end
def calendars
diff --git a/lib/tasks/erd.rake b/lib/tasks/erd.rake
index 6b79967de..e2665374e 100644
--- a/lib/tasks/erd.rake
+++ b/lib/tasks/erd.rake
@@ -7,9 +7,9 @@ namespace :generate do
sh "bundle exec rake erd only='Organisation,StopAreaReferential,StopAreaReferentialSync,StopAreaReferentialSyncMessage,StopAreaReferentialMembership,LineReferential,LineReferentialSync,LineReferentialSyncMessage,LineReferentialMembership' filename='referentiels_externes' title='Référentiels externes'"
sh "bundle exec rake erd only='NetexImport,Import,WorkbenchImport,ImportResource,ImportMessage' filename='import' title='Import'"
sh "bundle exec rake erd only='ComplianceControlSet,ComplianceControlBlock,ComplianceControl,ComplianceCheckSet,ComplianceCheckBlock,ComplianceCheck,ComplianceCheckResource,ComplianceCheckMessage' filename='validation' title='Validation'"
+ sh "bundle exec rake erd only='Organisation,Workbench,ReferentialSuite,Referential' filename='merge' title='Merge'"
#sh "bundle exec rake erd only='VehicleJourney,VehicleJourneyExport' filename='export' title='Export'"
- #sh "bundle exec rake erd only='' filename='intégration' title='Integration'"
- #sh "bundle exec rake erd only='' filename='fusion' title='Fusion'"
+ #sh "bundle exec rake erd only='' filename='integration' title='Integration'"
#sh "bundle exec rake erd only='' filename='publication' title='Publication'"
end
diff --git a/package.json b/package.json
index 7e8d695ac..01b1244af 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
},
"license": "MIT",
"engines": {
- "node": "6.11.4"
+ "node": "~6.11.4"
},
"devDependencies": {
"clean-webpack-plugin": "0.1.17",
diff --git a/spec/controllers/compliance_controls_controller_spec.rb b/spec/controllers/compliance_controls_controller_spec.rb
index 34b27530d..61e94025d 100644
--- a/spec/controllers/compliance_controls_controller_spec.rb
+++ b/spec/controllers/compliance_controls_controller_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe ComplianceControlsController, type: :controller do
describe 'POST #update' do
it 'should be successful' do
post :update, compliance_control_set_id: compliance_control_set.id, id: compliance_control.id, compliance_control: compliance_control.as_json.merge(type: 'GenericAttributeControl::MinMax')
- expect(response).to redirect_to compliance_control_set_compliance_control_path(compliance_control_set, compliance_control)
+ expect(response).to redirect_to compliance_control_set_path(compliance_control_set)
end
end
diff --git a/spec/factories/imports.rb b/spec/factories/imports.rb
index 2c53106c3..e07447b60 100644
--- a/spec/factories/imports.rb
+++ b/spec/factories/imports.rb
@@ -5,6 +5,23 @@ FactoryGirl.define do
current_step_progress 1.5
association :workbench
association :referential
+ file {File.open(File.join(Rails.root, 'spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip'))}
+ status :new
+ started_at nil
+ ended_at nil
+ creator 'rspec'
+
+ after(:build) do |import|
+ import.class.skip_callback(:create, :before, :initialize_fields)
+ end
+ end
+
+ factory :bad_import do
+ sequence(:name) { |n| "Import #{n}" }
+ current_step_id "MyString"
+ current_step_progress 1.5
+ association :workbench
+ association :referential
file {File.open(File.join(Rails.root, 'spec', 'fixtures', 'terminated_job.json'))}
status :new
started_at nil
diff --git a/spec/factories/netex_imports.rb b/spec/factories/netex_imports.rb
index 057e47730..9e9d836e4 100644
--- a/spec/factories/netex_imports.rb
+++ b/spec/factories/netex_imports.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
factory :netex_import, class: NetexImport, parent: :import do
- file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) }
+ file { File.open(Rails.root.join('spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip')) }
end
end
diff --git a/spec/factories/workbench_imports.rb b/spec/factories/workbench_imports.rb
index 5cdcfd15f..466bfe688 100644
--- a/spec/factories/workbench_imports.rb
+++ b/spec/factories/workbench_imports.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
factory :workbench_import, class: WorkbenchImport, parent: :import do
- file { File.open(Rails.root.join('spec', 'fixtures', 'terminated_job.json')) }
+ file { File.open(Rails.root.join('spec', 'fixtures', 'OFFRE_TRANSDEV_2017030112251.zip')) }
end
end
diff --git a/spec/fixtures/OFFRE_WITH_EXTRA.zip b/spec/fixtures/OFFRE_WITH_EXTRA.zip
new file mode 100644
index 000000000..97ea3f513
--- /dev/null
+++ b/spec/fixtures/OFFRE_WITH_EXTRA.zip
Binary files differ
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
index cd5a30982..c06d05dab 100644
--- a/spec/models/import_spec.rb
+++ b/spec/models/import_spec.rb
@@ -10,6 +10,9 @@ RSpec.describe Import, type: :model do
it { should validate_presence_of(:workbench) }
it { should validate_presence_of(:creator) }
+ it { should allow_value('file.zip').for(:file).with_message(I18n.t('activerecord.errors.models.imports.wrong_file_extension')) }
+ it { should_not allow_values('file.json', 'file.png', 'file.pdf').for(:file) }
+
let(:workbench_import) { build_stubbed(:workbench_import) }
let(:workbench_import_with_completed_steps) do
workbench_import = build_stubbed(
diff --git a/spec/services/zip_service/regression_4273_spec.rb b/spec/services/zip_service/regression_4273_spec.rb
deleted file mode 100644
index 4fe0f6539..000000000
--- a/spec/services/zip_service/regression_4273_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-RSpec.describe ZipService do
- describe 'Regression Issue # 4273 https://projects.af83.io/issues/4273' do
- let( :zip_service ){ described_class }
- let( :unzipper ){ zip_service.new(zip_data) }
- let( :zip_data ){ File.read zip_file }
-
- context 'real test data' do
- let( :subdir_names ){ %w<OFFRE_TRANSDEV_20170301122517 OFFRE_TRANSDEV_20170301122519> }
- let( :expected_chksums ){
- checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'source_') } )
- }
-
- let( :zip_file ){ fixtures_path 'OFFRE_TRANSDEV_2017030112251.zip' }
- #
- # Remove potential test artefacts
- before do
- subdir_names.each do | subdir_name |
- File.unlink( subdir_file subdir_name, suffix: '.zip' ) rescue nil
- Dir.unlink( subdir_file subdir_name ) rescue nil
- end
- end
-
- it "yields the correct content" do
- subdir_contents = {}
- # Write ZipService Streams to files and inflate them to file system
- unzipper.subdirs.each do | subdir |
- File.open(subdir_file( subdir.name, suffix: '.zip' ), 'wb'){ |f| f.write subdir.stream.string }
- unzip_subdir subdir
- end
- # Represent the inflated file_system as a checksum tree
- actual_checksums =
- checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'target/') } )
- expect( actual_checksums ).to eq( expected_chksums )
- end
-
- end
-
- end
-
- def checksum_trees *dirs
- dirs.flatten.inject({},&method(:checksum_tree))
- end
- def checksum_tree repr, dir
- Dir.glob("#{dir}/**/*").each do |file|
- if !File.directory?(file)
- repr.merge!( File.basename(file) => %x{cksum #{file}}.split.first ){ |_, ov, nv| Array(ov) << nv }
- end
- end
- repr
- end
-
- def subdir_file( subdir, prefix: 'target_', suffix: '' )
- fixtures_path("#{prefix}#{subdir}#{suffix}")
- end
-
- def unzip_subdir subdir
- %x{unzip -oqq #{subdir_file subdir.name, suffix: '.zip'} -d #{fixture_path}/target}
- end
-end
diff --git a/spec/services/zip_service_spec.rb b/spec/services/zip_service_spec.rb
new file mode 100644
index 000000000..98cb9026d
--- /dev/null
+++ b/spec/services/zip_service_spec.rb
@@ -0,0 +1,68 @@
+RSpec.describe ZipService do
+
+ let( :zip_service ){ described_class }
+ let( :unzipper ){ zip_service.new(zip_data) }
+ let( :zip_data ){ File.read zip_file }
+
+
+ context 'correct test data' do
+ before do
+ subdir_names.each do | subdir_name |
+ File.unlink( subdir_file subdir_name, suffix: '.zip' ) rescue nil
+ Dir.unlink( subdir_file subdir_name ) rescue nil
+ end
+ end
+ let( :subdir_names ){ %w<OFFRE_TRANSDEV_20170301122517 OFFRE_TRANSDEV_20170301122519> }
+ let( :expected_chksums ){
+ checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'source_') } )
+ }
+
+ let( :zip_file ){ fixtures_path 'OFFRE_TRANSDEV_2017030112251.zip' }
+ #
+ # Remove potential test artefacts
+
+ it 'yields the correct content' do
+ # Write ZipService Streams to files and inflate them to file system
+ unzipper.subdirs.each do | subdir |
+ expect( subdir.spurious ).to be_empty
+ File.open(subdir_file( subdir.name, suffix: '.zip' ), 'wb'){ |f| f.write subdir.stream.string }
+ unzip_subdir subdir
+ end
+ # Represent the inflated file_system as a checksum tree
+ actual_checksums =
+ checksum_trees( subdir_names.map{ |sn| subdir_file(sn, prefix: 'target/') } )
+ expect( actual_checksums ).to eq( expected_chksums )
+ end
+
+ end
+
+ context 'test data with spurious directories' do
+ let( :zip_file ){ fixtures_path 'OFFRE_WITH_EXTRA.zip' }
+
+ it 'returns the extra dir in the spurious field of the entry' do
+ expect( unzipper.subdirs.first.spurious ).to eq(%w{EXTRA})
+ end
+ end
+
+
+ def checksum_trees *dirs
+ dirs.flatten.inject({},&method(:checksum_tree))
+ end
+ def checksum_tree repr, dir
+ Dir.glob("#{dir}/**/*").each do |file|
+ if !File.directory?(file)
+ repr.merge!( File.basename(file) => %x{cksum #{file}}.split.first ){ |_, ov, nv| Array(ov) << nv }
+ end
+ end
+ repr
+ end
+
+ def subdir_file( subdir, prefix: 'target_', suffix: '' )
+ fixtures_path("#{prefix}#{subdir}#{suffix}")
+ end
+
+ def unzip_subdir subdir
+ %x{unzip -oqq #{subdir_file subdir.name, suffix: '.zip'} -d #{fixture_path}/target}
+ end
+end
+
diff --git a/spec/support/random.rb b/spec/support/random.rb
index 59e1a1475..0ebc2ee5e 100644
--- a/spec/support/random.rb
+++ b/spec/support/random.rb
@@ -22,6 +22,12 @@ module Support
def random_string
SecureRandom.urlsafe_base64
end
+
+ def very_random(veryness=3, joiner: '-')
+ raise ArgumentError, 'not very random' unless veryness > 1
+ veryness.times.map{ SecureRandom.uuid }.join(joiner)
+ end
+
end
end
diff --git a/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb b/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb
new file mode 100644
index 000000000..5e34b208a
--- /dev/null
+++ b/spec/workers/workbench_import/workbench_import_with_corrupt_zip_spec.rb
@@ -0,0 +1,48 @@
+RSpec.describe WorkbenchImportWorker do
+
+
+ shared_examples_for 'corrupt zipfile data' do
+ subject { described_class.new }
+ let( :workbench_import ){ create :workbench_import, status: :pending }
+
+ before do
+ # Let us make sure that the name Enterprise will never be forgotten by history,
+ # ahem, I meant, that nothing is uploaded, by forbidding any message to be sent
+ # to HTTPService
+ expect_it.to receive(:download).and_return(downloaded)
+ end
+
+ it 'does not upload' do
+ stub_const 'HTTPService', double('HTTPService')
+ subject.perform(workbench_import.id)
+ end
+
+ it 'does create a message' do
+ expect{ subject.perform(workbench_import.id) }.to change{ workbench_import.messages.count }.by(1)
+
+ message = workbench_import.messages.last
+ expect( message.criticity ).to eq('error')
+ expect( message.message_key ).to eq('corrupt_zip_file')
+ expect( message.message_attributes ).to eq( 'import_name' => workbench_import.name )
+ end
+
+ it 'does not change current step' do
+ expect{ subject.perform(workbench_import.id) }.not_to change{ workbench_import.current_step }
+ end
+
+ it "sets the workbench_import.status to failed" do
+ subject.perform(workbench_import.id)
+ expect( workbench_import.reload.status ).to eq('failed')
+ end
+ end
+
+ context 'empty zip file' do
+ let( :downloaded ){ '' }
+ it_should_behave_like 'corrupt zipfile data'
+ end
+
+ context 'corrupt data' do
+ let( :downloaded ){ very_random }
+ it_should_behave_like 'corrupt zipfile data'
+ end
+end
diff --git a/spec/workers/workbench_import_worker_spec.rb b/spec/workers/workbench_import/workbench_import_worker_spec.rb
index a349b3433..deaa1e3a5 100644
--- a/spec/workers/workbench_import_worker_spec.rb
+++ b/spec/workers/workbench_import/workbench_import_worker_spec.rb
@@ -5,7 +5,7 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do
let( :workbench ){ import.workbench }
let( :referential ){ import.referential }
- let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{SecureRandom.hex}" }
+ let( :api_key ){ build_stubbed :api_key, referential: referential, token: "#{referential.id}-#{random_hex}" }
# http://www.example.com/workbenches/:workbench_id/imports/:id/download
let( :host ){ Rails.configuration.rails_host }
@@ -13,16 +13,17 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do
let( :downloaded_zip ){ double("downloaded zip") }
let( :download_zip_response ){ OpenStruct.new( body: downloaded_zip ) }
- let( :download_token ){ SecureRandom.urlsafe_base64 }
-
+ let( :download_token ){ random_string }
let( :upload_path ) { api_v1_netex_imports_path(format: :json) }
+ let( :spurious ){ [[], [], []] }
let( :subdirs ) do
entry_count.times.map do |i|
ZipService::Subdir.new(
"subdir #{i}",
- double("subdir #{i}", rewind: 0, read: '')
+ double("subdir #{i}", rewind: 0, read: ''),
+ spurious[i]
)
end
end
@@ -104,8 +105,44 @@ RSpec.describe WorkbenchImportWorker, type: [:worker, :request] do
expect( import ).to receive(:update).with(current_step: 3, status: 'failed')
expect { worker.perform import.id }.to raise_error(StopIteration)
+ end
+ end
+
+ context 'multireferential zipfile with spurious directories' do
+ let( :entry_count ){ 2 }
+ let( :spurious1 ){ [random_string] }
+ let( :spurious2 ){ [random_string, random_string] }
+ let( :spurious ){ [spurious1, spurious2] }
+ let( :messages ){ double('messages') }
+ let( :message_attributes ){{criticity: :warning, message_key: 'inconsistent_zip_file'}}
+ let( :message1_attributes ){ message_attributes.merge(message_attributes: {'import_name' => import.name, 'spurious_dirs' => spurious1.join(', ')}) }
+ let( :message2_attributes ){ message_attributes.merge(message_attributes: {'import_name' => import.name, 'spurious_dirs' => spurious2.join(', ')}) }
+
+ before do
+ allow(import).to receive(:messages).and_return(messages)
+ end
+
+ it 'downloads a zip file, cuts it, and uploads all pieces and adds messages' do
+
+ expect(HTTPService).to receive(:get_resource)
+ .with(host: host, path: path, params: {token: download_token})
+ .and_return( download_zip_response )
+
+ subdirs.each do |subdir|
+ mock_post subdir, post_response_ok
+ end
+
+ expect( import ).to receive(:update).with(total_steps: 2)
+ expect( import ).to receive(:update).with(current_step: 1)
+ expect( messages ).to receive(:create).with(message1_attributes)
+ expect( import ).to receive(:update).with(current_step: 2)
+ expect( messages ).to receive(:create).with(message2_attributes)
+ expect( import ).to receive(:update).with(ended_at: Time.now)
+
+ worker.perform import.id
end
+
end
def mock_post subdir, response