diff options
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">×</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">×</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">×</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">×</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">×</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">×</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">×</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.zipBinary files differ new file mode 100644 index 000000000..97ea3f513 --- /dev/null +++ b/spec/fixtures/OFFRE_WITH_EXTRA.zip 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 | 
