diff options
Diffstat (limited to 'app')
382 files changed, 7727 insertions, 2717 deletions
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4c5aff22f..6a79f7e8e 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -22,3 +22,6 @@ //= require_directory . // require('whatwg-fetch') // require('babel-polyfill') +//= require "i18n" +//= require "i18n/extended" +//= require "i18n/translations" diff --git a/app/assets/javascripts/forms.coffee b/app/assets/javascripts/forms.coffee index 12d82fef1..b7ae3c6ca 100644 --- a/app/assets/javascripts/forms.coffee +++ b/app/assets/javascripts/forms.coffee @@ -32,14 +32,25 @@ isEdge = !isIE && !!window.StyleMedia @colorSelector = -> $('.form-group .dropdown.color_selector').each -> - selectedStatus = $(this).children('.dropdown-toggle').children('.fa-circle') - + selectedStatusColor = $(this).children('.dropdown-toggle').children('.fa-circle') + selectedStatusLabel = $(this).children('.dropdown-toggle') + self = this $(this).on 'click', "input[type='radio']", (e) -> selectedValue = e.currentTarget.value + selectedText = $(e.currentTarget).parent()[0].textContent + if e.currentTarget.getAttribute("data-for") + hidden = $("[name=\"#{e.currentTarget.getAttribute("data-for")}\"]") + if selectedValue == '' - $(selectedStatus).css('color', 'transparent') + $(selectedStatusColor).css('color', 'transparent') + $(selectedStatusLabel).contents().filter( -> this.nodeType == 3 ).filter(':first').text = "" + hidden?.val "" else - $(selectedStatus).css('color', selectedValue) + $(selectedStatusColor).css('color', selectedValue) + $(selectedStatusLabel).contents().filter( -> this.nodeType == 3 ).first().replaceWith selectedText + hidden?.val selectedValue + + $(self).find('.dropdown-toggle').click() $ -> togglableFilter() diff --git a/app/assets/javascripts/i18n/extended.coffee b/app/assets/javascripts/i18n/extended.coffee new file mode 100644 index 000000000..aeb67bd09 --- /dev/null +++ b/app/assets/javascripts/i18n/extended.coffee @@ -0,0 +1,24 @@ +#= require i18n + +decorateI18n = (_i18n)-> + _i18n.tc = (key, opts={}) -> + out = _i18n.t(key, opts) + out += " " if _i18n.locale == "fr" + out + ":" + + _i18n.model_name = (model, opts={}) -> + last_key = if opts.plural then "other" else "one" + _i18n.t("activerecord.models.#{model}.#{last_key}") + + _i18n.attribute_name = (model, attribute, opts={}) -> + _i18n.t("activerecord.attributes.#{model}.#{attribute}") + + _i18n.enumerize = (enumerize, key, opts={}) -> + I18n.t("enumerize.#{enumerize}.#{key}") + + _i18n + +module?.exports = decorateI18n + +if I18n? + decorateI18n(I18n) diff --git a/app/assets/javascripts/main_menu.coffee b/app/assets/javascripts/main_menu.coffee index a12c47576..e943f448a 100644 --- a/app/assets/javascripts/main_menu.coffee +++ b/app/assets/javascripts/main_menu.coffee @@ -1,12 +1,11 @@ $ -> - link = [] + stickyActions = [] ptitleCont = "" $(document).on 'page:before-change', -> - link = [] + stickyActions = [] ptitleCont = "" - $el = $('#main_nav') # Opening/closing left-side menu $el.find('.openMenu').on 'click', (e) -> @@ -24,34 +23,45 @@ $ -> limit = 51 if $(window).scrollTop() >= limit - if ($('.page-action .small').length > 0) - data = $('.page-action .small')[0].innerHTML + if stickyActions.length == 0 + if ($('.page-action .small').length > 0) + stickyActions.push + content: [ + $('.page-action .small'), + $('.page-action .small').first().next() + ] + originalParent: $('.page-action .small').parent() + + for action in $(".sticky-action, .sticky-actions") + stickyActions.push + class: "small", + content: [$(action)] + originalParent: $(action).parent() - if ($(".page-title").length > 0) + if $(".page-title").length > 0 ptitleCont = $(".page-title").html() - stickyContent = '<div class="sticky-content">' - stickyContent += '<div class="sticky-ptitle">' + ptitleCont + '</div>' - stickyContent += '<div class="sticky-paction"><div class="small">' + data + '</div></div>' - stickyContent += '</div>' + stickyContent = $('<div class="sticky-content"></div>') + stickyContent.append $("<div class='sticky-ptitle'>#{ptitleCont}</div>") + stickyContent.append $('<div class="sticky-paction"></div>') $('#main_nav').addClass 'sticky' if $('#menu_top').find('.sticky-content').length == 0 if ptitleCont.length > 0 $('#menu_top').children('.menu-content').after(stickyContent) - if link.length == 0 - link = $('.page-action .small').next() - - $('.sticky-paction .small').after(link) + for item in stickyActions + for child in item.content + child.appendTo $('.sticky-paction') else $('#main_nav').removeClass 'sticky' if $('#menu_top').find('.sticky-content').length > 0 - if !$('.page-action').find('.formSubmitr').length - $('.page-action .small').after(link) + for item in stickyActions + for child in item.content + child.appendTo item.originalParent $('.sticky-content').remove() - sticker(); + sticker() # Sticky behavior $(document).on 'scroll', sticker diff --git a/app/assets/javascripts/select2.coffee b/app/assets/javascripts/select2.coffee index 2e3884d7f..05b73dc6b 100644 --- a/app/assets/javascripts/select2.coffee +++ b/app/assets/javascripts/select2.coffee @@ -9,18 +9,18 @@ bind_select2 = (el, cfg = {}) -> target.select2 $.extend({}, default_cfg, cfg) bind_select2_ajax = (el, cfg = {}) -> - target = $(el) + _this = $(el) cfg = ajax: data: (params) -> - q: - "#{target.data('term')}": params.term - url: target.data('url'), + if _this.data('term') + { q: "#{_this.data('term')}": params.term } + else + { q: params.term } + url: _this.data('url'), dataType: 'json', delay: 125, processResults: (data, params) -> results: data - minimumInputLength: 1 - placeholder: target.data('select2ed-placeholder') templateResult: (item) -> item.text templateSelection: (item) -> @@ -28,6 +28,9 @@ bind_select2_ajax = (el, cfg = {}) -> escapeMarkup: (markup) -> markup + if _this.data('initvalue') + cfg["initSelection"] = (item, callback) -> callback(_this.data('initvalue')) + bind_select2(el, cfg) @select_2 = -> @@ -40,7 +43,5 @@ bind_select2_ajax = (el, cfg = {}) -> $('select.form-control.tags').each -> bind_select2(this, {tags: true}) - - $ -> select_2() diff --git a/app/assets/stylesheets/OpenLayers/custom.sass b/app/assets/stylesheets/OpenLayers/custom.sass index 7a5b4baf1..fa874d924 100644 --- a/app/assets/stylesheets/OpenLayers/custom.sass +++ b/app/assets/stylesheets/OpenLayers/custom.sass @@ -2,6 +2,26 @@ .list-group-item & margin-top: 15px + .routes-labels + padding: 0 + display: flex + justify-content: space-between + flex-wrap: wrap + margin: 5px -5px + li + list-style-type: none + flex: 1 0 25% + border: 1px solid $lightgrey + padding: 5px + margin: 5px + border-radius: 5px + cursor: pointer + color: $blue + white-space: nowrap + + &:hover + background: $orange + color: white .ol-scale-line background-color: transparent diff --git a/app/assets/stylesheets/_layout.sass b/app/assets/stylesheets/_layout.sass index b6b91b2a5..340467e77 100644 --- a/app/assets/stylesheets/_layout.sass +++ b/app/assets/stylesheets/_layout.sass @@ -28,6 +28,7 @@ body // width: 75% border-bottom: 1px solid rgba($blue, 0.5) margin: 30px auto 45px auto + clear: both .content_header font-size: 2.2rem diff --git a/app/assets/stylesheets/application.sass b/app/assets/stylesheets/application.sass index 50f8b64cb..3f8467efe 100644 --- a/app/assets/stylesheets/application.sass +++ b/app/assets/stylesheets/application.sass @@ -19,3 +19,5 @@ @import 'modules/vj_collection' @import 'modules/timetables' @import 'modules/import_messages' + +@import 'flag-icon' diff --git a/app/assets/stylesheets/base/_config.sass b/app/assets/stylesheets/base/_config.sass index 65444479f..0fff1dd9c 100644 --- a/app/assets/stylesheets/base/_config.sass +++ b/app/assets/stylesheets/base/_config.sass @@ -16,6 +16,8 @@ $blue: #007fbb $darkgrey: #4b4b4b $grey: #a4a4a4 +$lightgrey: lighten($grey, 25) +$lightergrey: lighten($lightgrey,5) $green: #70b12b $red: #da2f36 diff --git a/app/assets/stylesheets/base/_utilities.sass b/app/assets/stylesheets/base/_utilities.sass index 24f2038f0..fbda5630d 100644 --- a/app/assets/stylesheets/base/_utilities.sass +++ b/app/assets/stylesheets/base/_utilities.sass @@ -44,7 +44,7 @@ // Empty zones =emptyzone($col1, $col2) - background-image: linear-gradient(-45deg, $col1 25%, $col2 25%, $col2 50%, $col1 50%, $col1 75%, $col2 75%, transparent) + background-image: linear-gradient(-45deg, $col1 25%, $col2 25%, $col2 50%, $col1 50%, $col1 75%, $col2 75%, $col2) background-size: 40px 40px .cellwrap diff --git a/app/assets/stylesheets/components/_breadcrumb.sass b/app/assets/stylesheets/components/_breadcrumb.sass index 62f167eb4..1b30ca42b 100644 --- a/app/assets/stylesheets/components/_breadcrumb.sass +++ b/app/assets/stylesheets/components/_breadcrumb.sass @@ -1,3 +1,6 @@ .breadcrumbs + white-space: nowrap + text-overflow: ellipsis + overflow: hidden a - color: white
\ No newline at end of file + color: white diff --git a/app/assets/stylesheets/components/_buttons.sass b/app/assets/stylesheets/components/_buttons.sass index a59699383..2881cee3e 100644 --- a/app/assets/stylesheets/components/_buttons.sass +++ b/app/assets/stylesheets/components/_buttons.sass @@ -143,28 +143,49 @@ table, .table border-radius: 0 box-shadow: 0 0 3px rgba($darkgrey, 0.25) + & > ul:not(:first-child) + position: relative + margin-top: 11px + &:before + content: '' + display: block + position: absolute + left: 15px + right: 15px + top: -6px + height: 1px + background-color: $grey + + ul.dropdown-menu, .dropdown-menu > ul + padding: 0 + margin: 0 > li > a, > li > button padding: 5px 15px + white-space: nowrap + padding: 5px 15px + font-weight: normal + line-height: $line-height + display: block + font-size: 14px + &:hover, &:focus + text-decoration: none + background-color: whitesmoke + outline: none > li.delete-action > a, > button display: block position: relative - margin-top: 11px - - &:before - content: '' - display: block - position: absolute - left: 15px - right: 15px - top: -6px - height: 1px - background-color: $grey - .fa:first-child margin-right: 0.5em + & + li.delete-action + > a, > button + margin-top: 0 + &:before + display: none + + &.table-2entries .t2e-item > .th position: relative diff --git a/app/assets/stylesheets/components/_color_selector.sass b/app/assets/stylesheets/components/_color_selector.sass new file mode 100644 index 000000000..07bfa0c80 --- /dev/null +++ b/app/assets/stylesheets/components/_color_selector.sass @@ -0,0 +1,21 @@ +select.color_selector + option[value='#9B9B9B'] + background-color: #9B9B9B + option[value='#FFA070'] + background-color: #FFA070 + option[value='#C67300'] + background-color: #C67300 + option[value='#7F551B'] + background-color: #7F551B + option[value='#41CCE3'] + background-color: #41CCE3 + option[value='#09B09C'] + background-color: #09B09C + option[value='#3655D7'] + background-color: #3655D7 + option[value='#6321A0'] + background-color: #6321A0 + option[value='#E796C6'] + background-color: #E796C6 + option[value='#DD2DAA'] + background-color: #DD2DAA
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_dropdown.sass b/app/assets/stylesheets/components/_dropdown.sass index 8a8d69063..a0d217b14 100644 --- a/app/assets/stylesheets/components/_dropdown.sass +++ b/app/assets/stylesheets/components/_dropdown.sass @@ -18,9 +18,19 @@ background-color: whitesmoke outline: none - > .disabled > a, > .disabled > button - cursor: not-allowed - &, &:hover, &:focus - color: rgba($darkgrey, 0.5) - background-color: transparent - outline: none + &, & > ul + > .disabled > a, > .disabled > button + cursor: not-allowed + &, &:hover, &:focus + color: rgba($darkgrey, 0.5) + background-color: transparent + outline: none + + > ul > li > a + display: block + padding: 3px 20px + clear: both + font-weight: normal + line-height: 1.42857 + color: #333333 + white-space: nowrap diff --git a/app/assets/stylesheets/components/_forms.sass b/app/assets/stylesheets/components/_forms.sass index 9a363ab97..caa8ac0e4 100644 --- a/app/assets/stylesheets/components/_forms.sass +++ b/app/assets/stylesheets/components/_forms.sass @@ -85,9 +85,15 @@ input // BS horizontal form label positionning fix .form-horizontal + input[type="radio"].form-control + height: auto + width: auto .form-group position: relative - + .radio-inline + padding-top: 4px + &:first-child + padding-left: 0 > .control-label &[class*='col-sm-'] float: none @@ -229,6 +235,13 @@ $cbx-size-xs: 15px &[type='checkbox']:checked + label:before background-color: $blue + &[type='checkbox']:disabled + label + &:before + border-color: $grey + background-color: $lightgrey + cursor: not-allowed + &:after + display: none // Table adjustments table, .table .td, td, .th, th @@ -243,8 +256,15 @@ table, .table &.table-2entries .t2e-item > .th position: relative - - > .checkbox + > .st_action + list-style-type: none + button + border-radius: 50% + background: $blue + color: white + border: none + + > .checkbox, > .st_action position: absolute right: 0 top: 0 @@ -277,6 +297,7 @@ table, .table height: $cbx-size width: $cbx-size margin: 0 auto + transition: transform 0.2s, background-color 0.2s > input[type='checkbox'] &:not(:checked), &:checked @@ -436,6 +457,19 @@ table, .table margin: 0 min-height: 41px padding: 5px 15px + &.active + &:after + position: absolute + top: 0 + left: 0 + right: 0 + height: 4px + background: $blue + content: "" + &.per-page-select + padding-top: 10px + .selected + font-weight: bold .control-label font-weight: 700 @@ -454,6 +488,20 @@ table, .table > .form-group.select2ed width: 300px + &.name-filter + .checkbox_list + .form-group + padding: 10px + width: 100px + &.to + width: 20px + color: $grey + text-align: center + input + width: 100% + & + .form-group + padding-left: 0px + > .actions position: absolute right: 15px diff --git a/app/assets/stylesheets/components/_lists.sass b/app/assets/stylesheets/components/_lists.sass index d8f83d72b..3cce20021 100644 --- a/app/assets/stylesheets/components/_lists.sass +++ b/app/assets/stylesheets/components/_lists.sass @@ -54,3 +54,8 @@ $dlWidth: 40% // Definition .dl-def width: 100% - $dlWidth + + ul + list-style: none + padding-left: 0 + margin-bottom: 0
\ No newline at end of file diff --git a/app/assets/stylesheets/components/_main_nav.sass b/app/assets/stylesheets/components/_main_nav.sass index fdbf5836a..2af070389 100644 --- a/app/assets/stylesheets/components/_main_nav.sass +++ b/app/assets/stylesheets/components/_main_nav.sass @@ -17,6 +17,9 @@ $menuW: 300px line-height: $menuH padding-left: 10px opacity: 0.6 + > a + color: rgba(#fff, 0.9) + text-decoration: none #menu_left position: absolute @@ -240,6 +243,9 @@ $menuW: 300px left: 0 top: 13px + & > .menu-item + max-width: 75% + .menu-item padding: 0 10px @@ -321,54 +327,45 @@ $menuW: 300px height: $menuH * 2 transition: 0.1s - #menu_top > .menu-content > .menu-item-group - display: none + #menu_top > .menu-content + & > .menu-item + max-width: 90% + & > .menu-item-group + display: none .sticky-content height: $menuH padding: 0 50px 0 75px margin-top: -4px + display: flex > * display: inline-block &.sticky-ptitle - width: 60% + flex: 1 1 height: $menuH + position: relative h1 - position: relative + position: absolute + left: 0 + right: 10px + top: 0 line-height: 1.1 white-space: nowrap max-height: 1.1em margin: 0 -1.4em 0 0 - padding: 0 1.4em 0 0 + padding: 0 1.4em 5px 0 overflow: hidden - - &:before - content: '[...]' - font-size: 0.65em - position: absolute - z-index: 5 - right: 0 - bottom: 4px - - &:after - content: '' - position: absolute - z-index: 5 - right: 0 - width: 1.4em - height: 1em - margin-top: 0.2em - background-color: $blue + text-overflow: ellipsis .small.fa color: #fff margin-left: 0.5em &.sticky-paction - width: 40% + flex: 0 0 auto text-align: right vertical-align: top diff --git a/app/assets/stylesheets/components/_panels.sass b/app/assets/stylesheets/components/_panels.sass index e9f615081..ab25d8184 100644 --- a/app/assets/stylesheets/components/_panels.sass +++ b/app/assets/stylesheets/components/_panels.sass @@ -34,6 +34,7 @@ a text-decoration: none color: $blue + text-transform: capitalize &:hover, &:focus color: $darkblue diff --git a/app/assets/stylesheets/components/_referential_overview.sass b/app/assets/stylesheets/components/_referential_overview.sass new file mode 100644 index 000000000..0beb8ab67 --- /dev/null +++ b/app/assets/stylesheets/components/_referential_overview.sass @@ -0,0 +1,346 @@ +.referential-overview + $left-size: 100px + $line-height: 60px + margin-top: 50px + overflow: hidden + .time-travel, .filters + background-color: $lightergrey + padding: 10px + float: right + border-top-left-radius: 4px + border-top-right-radius: 4px + border: 1px solid $lightgrey + border-bottom: none + position: relative + &:after + position: absolute + content: "" + left: 0 + top: 100% + right: 0 + height: 10px + box-shadow: 0 0 10px rgba(0,0,0,0.5) + z-index: 1 + .time-travel + padding-top: 4px + padding-bottom: 4px + a.btn:first-child + margin-right: 1px + a.btn:last-child + margin-right: 1px + + max-width: 33% + .btn-group, .form-group + position: relative + z-index: 2 + .form-group + margin-left: 10px + margin-bottom: 0 + display: inline-block + input + padding: 6px 5px + border: 1px solid $lightgrey + outline: none + height: 34px + border-radius: 4px + padding-right: 25px + a + padding: 4px + margin-top: 2px + margin-left: -25px + .filters + float: left + max-width: 66% + padding: 0 + form + background: transparent + display: flex + .ffg-row + border-color: $grey + .form-group + border-color: $grey + width: auto + flex: 1 1 + padding: 4px 11px 5px + .input-group-btn + right: 10px + &.togglable + padding-top: 6px + padding-bottom: 5px + &:before + top: 0px + + .overview-table + position: relative + z-index: 2 + border: 1px solid $grey + clear: both + display: flex + +emptyzone($lightgrey, $lightergrey) + .head + + height: $left-size + .line + height: $line-height + .left + flex: 0 0 + background: $lightergrey + min-width: $left-size + overflow: hidden + border-right: 1px solid white + .head + position: relative + border-bottom: 1px solid $grey + border-right: 1px solid $lightgrey + .dates, .lines + position: absolute + font-size: 0.8em + z-index: 2 + .dates + right: 20px + top: 20px + .lines + left: 20px + bottom: 20px + &:after + position: absolute + border-left: ($left-size - 2px)/2 solid transparent + border-bottom: ($left-size - 2px)/2 solid transparent + border-right: ($left-size - 2px)/2 solid white + border-top: ($left-size - 2px)/2 solid white + z-index: 1 + top: 0 + right: 0 + width: 0 + content: "" + .line + padding: 7px 10px + border-bottom: 1px solid $grey + font-size: 0.8em + &:last-child + border-bottom: none + .number + border-radius: 100px + display: inline-block + min-width: 20px + height: 20px + text-align: center + padding: 1px 4px + text-decoration: none + color: black + border: 1px solid $grey + max-width: 100% + white-space: nowrap + text-overflow: ellipsis + overflow: hidden + .name + display: inline-block + width: $left-size - 50px() + white-space: nowrap + line-height: 20px + margin-left: 5px + text-overflow: ellipsis + overflow: hidden + vertical-align: bottom + color: black + text-decoration: none + + .company, .mode + font-size: 0.9em + white-space: nowrap + text-overflow: ellipsis + overflow: hidden + margin-top: -2px + .mode + text-transform: uppercase + color: $grey + font-weight: bold + .right + flex: 1 1 + overflow: hidden + .inner .lines + transition: margin-left 0.5s + .head + white-space: nowrap + position: relative + z-index: 3 + &:after, &:before + opacity: 0 + // transition: opacity 0.5s + content: "" + position: absolute + left: -1000px + right: 100% + top: 0px + bottom: 0 + background: $darkblue + z-index: 11 + border-top: 1px solid white + .week + display: inline-block + position: relative + height: 100% + transition: margin 0.5s + background: white + &:last-child + box-shadow: 0 -10px 10px rgba(0,0,0,0.5) + .week-span + left: 15px + top: 15px + right: 30px + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + position: absolute + + .week-number + background-color: $lightgrey + color: $grey + position: absolute + top: 0 + right: 0 + padding: 2px 4px + + &:after + position: absolute + right: 0 + top: 0 + bottom: 0 + background: $grey + width: 1px + content: "" + + &:last-child:after + display: none + + .days + position: relative + top: 50% + height: 50% + border-top: 1px solid $grey + border-bottom: 1px solid $grey + + .day + float: left + border-left: 1px solid $grey + box-sizing: border-box + padding-left: 5px + padding-top: 3px + position: relative + height: 100% + .name, .number + position: absolute + left: 10px + right: 10px + top: 50% + transform: translateY(-50%) + margin-top: 10px + .name + font-weight: bold + font-size: 0.8em + margin-top: -10px + &:first-child + border: none + &.weekend + background: $lightergrey + &.selected, &:hover + color: $blue + background-color: transparentize($blue, 0.7) + &:after + content: "" + left: -1px + right: -1px + top: 100% + height: 10000px + background-color: transparentize($blue, 0.7) + position: absolute + z-index: 4 + &:hover + background-color: transparentize(white, 0.7) + &:after + background-color: transparentize(white, 0.7) + + .line + border-bottom: 1px solid $grey + position: relative + overflow: hidden + box-shadow: 0 -10px 10px rgba(0,0,0,0.5) + + &:last-child + border-bottom: none + + .period + height: 100% + top: 0 + background: #aedd8a + position: absolute + box-shadow: 0 0 10px rgba(0,0,0,0.5) + z-index: 2 + .title + position: absolute + left: 12px + top: 50% + margin-top: -6px + transform: translateY(-50%) + background-color: transparentize(white, 0.25) + padding: 5px + font-size: 0.7em + border-radius: 5px + transition: margin-left 0.5s + max-width: calc(100% - 24px) + margin-right: 12px + &:after + content: "" + position: absolute + bottom: 1px + left: 0 + right: 0 + height: 10px + background: white + opacity: 0.25 + z-index: 3 + &.empty + z-index: 1 + background: rgb(244, 67, 67) + background: repeating-linear-gradient(-45deg, #f5e1cf,#f5e1cf 12px,#e49393 12px,#e49393 25px) + &.accepted + background: #f19039 + background: repeating-linear-gradient(-45deg, #f5e1cf,#f5e1cf 12px,#f19039 12px,#f19039 25px) + &:hover + z-index: 3 + &:after + opacity: 0.5 + .title + background-color: transparentize(white, 0.1) + + &.sticky + .time-travel + position: fixed + bottom: 0 + z-index: 15 + right: 35px + box-shadow: 0 0 10px rgba(0,0,0,0.5) + + .overview-table .right + .lines + margin-top: $left-size + .head + position: fixed + top: 80px + z-index: 10 + background: white + height: 50px + right: 51px + left: 51px + $left-size + // overflow-x: hidden + &:after, &:before + opacity: 1 + &:after + left: 100% + right: -1000px + .week-span, .week-number + display: none + .days + height: 100% + top: 0 + border-top: 1px solid white diff --git a/app/assets/stylesheets/components/_referentials.sass b/app/assets/stylesheets/components/_referentials.sass new file mode 100644 index 000000000..0bbb18f2b --- /dev/null +++ b/app/assets/stylesheets/components/_referentials.sass @@ -0,0 +1,4 @@ +#referential_form + .metadatas-errors + margin-top: -20px + margin-bottom: 20px diff --git a/app/assets/stylesheets/components/_select2.sass b/app/assets/stylesheets/components/_select2.sass index 332af16cd..f31387c9f 100644 --- a/app/assets/stylesheets/components/_select2.sass +++ b/app/assets/stylesheets/components/_select2.sass @@ -7,6 +7,11 @@ // .select2-results__message, .loading-results // display: none +.select2-results__option, .select2-selection__rendered + .label + font-size: 0.8em + margin-right: 0.5em + .select2-container, .select2-container--bootstrap .select2-selection--single .select2-selection__rendered height: 100% @@ -74,6 +79,9 @@ .select2-search--inline .select2-search__field height: 28px + .select2-selection__rendered + padding-right: 20px + .select2-container--bootstrap .select2-selection border-color: rgba($grey, 0.3) diff --git a/app/assets/stylesheets/components/_tables.sass b/app/assets/stylesheets/components/_tables.sass index 178ec2f36..1e02ad586 100644 --- a/app/assets/stylesheets/components/_tables.sass +++ b/app/assets/stylesheets/components/_tables.sass @@ -9,7 +9,6 @@ font-weight: 700 border-bottom: 2px solid $darkgrey vertical-align: middle - > a position: relative display: block @@ -211,6 +210,18 @@ top: 50% margin-top: -8px + .zdlp + background: url( image-path('map/zdlp.png') ) no-repeat left 50% + padding-left: 30px + + .lda + background: url( image-path('map/lda.png') ) no-repeat left 50% + padding-left: 30px + + .gdl + background: url( image-path('map/lda.png') ) no-repeat left 50% + padding-left: 30px + // select_toolbox .select_toolbox @@ -314,6 +325,26 @@ padding: 6px 8px border-bottom: 2px solid rgba($grey, 0.5) border-top: 1px solid rgba($grey, 0.5) + text-transform: capitalize + + .info-button + position: absolute + width: 20px + height: 20px + top: 0 + right: 0 + margin: 6px 8px + button + border: none + background: $blue + border-radius: 20px + width: 100% + height: 100% + font-size: 12px + line-height: 14px + color: white + outline: none + .td position: relative padding: 6px 8px @@ -360,7 +391,7 @@ white-space: nowrap // border-right: 1px solid rgba($grey, 0.5) max-width: 100% - min-width: 280px + min-width: 330px padding-right: 1px .t2e-item diff --git a/app/assets/stylesheets/components/_toolbar.sass b/app/assets/stylesheets/components/_toolbar.sass new file mode 100644 index 000000000..86a32bd82 --- /dev/null +++ b/app/assets/stylesheets/components/_toolbar.sass @@ -0,0 +1,49 @@ +#development-toolbar + .inner + overflow: scroll + padding: 10px + max-height: 70vh + display: flex + flex-direction: row + .toggles + font-size: 0.7em + float: right + a:first-child + padding-right: 3px + margin-right: 3px + border-right: 1px solid $lightgrey + + .col + flex: 1 1 + padding-right: 10px + padding-left: 10px + border-right: 1px solid $lightgrey + &:last-child + padding-right: 0 + border-right: none + + ul + padding: 0 + li + list-style-type: none + label + padding-left: 5px + font-weight: normal + & + a + float: right + h5 + font-weight: bold + &.permissions + ul + overflow: hidden + li + float: left + width: 33% + label + font-size: 0.8em + max-width: calc(100% - 15px) + text-overflow: ellipsis + overflow: hidden + padding-top: 3px + input + vertical-align: top diff --git a/app/assets/stylesheets/modules/_jp_collection.sass b/app/assets/stylesheets/modules/_jp_collection.sass index f579cf87b..46ea3fb6e 100644 --- a/app/assets/stylesheets/modules/_jp_collection.sass +++ b/app/assets/stylesheets/modules/_jp_collection.sass @@ -99,32 +99,204 @@ top: 50% margin-top: -8px - // Errors - .table-2entries .t2e-item-list - .t2e-item - position: relative - - .th .vj_tt - display: inline-block - vertical-align: top - - + .vj_tt - margin-left: 5px - - &.has-error - &:before - content: '' - position: absolute - top: 0 - left: 0 - right: 0 - bottom: 0 - border: 2px solid $red - - > .th - > div:first-child, > div:first-child + div - color: $red - - // Reset default behaviour - .form-control - border-color: #ccc + .table-2entries + .t2e-item-list + .td + overflow: hidden + + .t2e-item + position: relative + + .th .vj_tt + display: inline-block + vertical-align: top + + + .vj_tt + margin-left: 5px + + &.with-costs + .td + padding: 15px 8px + + .totals + color: $blue + padding-top: 4px + margin-left: -5px + margin-right: -5px + span + white-space: nowrap + padding: 0 5px + i + padding-right: 3px + + $link-size: 10px + .link + position: absolute + left: 50px + width: 10px + top: -15px + bottom: -15px + background: $blue + z-index: 3 + opacity: 0.5 + &:after + content: "" + width: $link-size + height: $link-size + position: absolute + top: 50% + bottom: 50% + margin-top: -$link-size/2 + border-top: $link-size/2 solid transparent + border-left: $link-size/2 solid transparent + border-right: $link-size/2 solid $blue + border-bottom: $link-size/2 solid $blue + transform: rotate(135deg) + left: 0% + opacity: 0 + transition: left 0.2s, opacity 0.2s + + .headlined .link + top: 0 + bottom: -15px + + &:after + top: 75% + margin-top: -$link-size/2 - 1px + + .activated .link + &:after + left: -50% + opacity: 1 + + & > div + position: relative + + .link + left: 35px + + .has_radio + margin-right: 150px + + .costs + background: $blue + opacity: 0.5 + padding: 5px + color: white + position: absolute + cursor: not-allowed + left: 75px + top: -1px + transform: translateY(-50%) + font-size: 0.75em + transition: background 0.1s + border: 1px solid white + + &:hover + opacity: 1 + &:after + opacity: 1 + + &:after + opacity: 0.5 + content: "" + height: 2px + position: absolute + left: -23px + background: $blue + right: 100% + top: 50% + transition: background 0.1s + + p + display: block + border: none + margin-bottom: 0 + i + margin-right: 3px + width: 12px + & + p + position: relative + z-index: 2 + padding-right: 0 + margin: 0 + border-right: none + + input + display: inline-block + width: 50px + border: none + margin-right: 5px + color: black + + .edit-mode + .costs + cursor: pointer + p + margin-bottom: 5px + & + p + margin-bottom: 0 + + opacity: 1 + &:after + opacity: 1 + + .link + opacity: 1 + + .with-headline + .costs + top: 25% + + .deactivated .costs + display: none + + &.has-error + &:before + content: '' + position: absolute + top: 0 + left: 0 + right: 0 + bottom: 0 + border: 2px solid $red + + > .th + > div:first-child, > div:first-child + div + color: $red + + // Reset default behaviour + .form-control + border-color: #ccc + + .t2e-head + .td.with-costs + & > div + &:not(.headlined) + height: calc(100% + 6px) + & > span + &:after + top: -15px + bottom: -9px + + div.headlined + &:before + margin-bottom: 0 + & > span + height: calc(100% - (1.4em + 15px)) + &:after + top: calc((1.4em + 30px) * -1) + bottom: 0 + + .td.with-costs, .with-costs .td + padding-top: 15px + padding-bottom: 15px + + & > div + height: calc(100% + 15px) + &.headlined + &:before + padding-top: 15px + padding-bottom: 15px + height: calc(1.4em + 30px) + margin-top: -15px + margin-bottom: 15px diff --git a/app/assets/stylesheets/modules/_timetables.sass b/app/assets/stylesheets/modules/_timetables.sass index b06972ef9..8999456ec 100644 --- a/app/assets/stylesheets/modules/_timetables.sass +++ b/app/assets/stylesheets/modules/_timetables.sass @@ -30,7 +30,7 @@ .t2e-item .th - padding: 6px 0 0 0 + padding: 4px 0 0 0 border-color: $darkgrey border-top-width: 2px diff --git a/app/assets/stylesheets/modules/_vj_collection.sass b/app/assets/stylesheets/modules/_vj_collection.sass index 56769e52b..d99c67bd7 100644 --- a/app/assets/stylesheets/modules/_vj_collection.sass +++ b/app/assets/stylesheets/modules/_vj_collection.sass @@ -9,6 +9,9 @@ position: relative padding-left: 25px + .fa + margin-left: 5px + > .headlined &:before margin-left: -25px @@ -113,6 +116,9 @@ margin-left: 5px &.has-error + .errors + color: $red + font-size: 0.8em &:before content: '' position: absolute diff --git a/app/assets/stylesheets/modules/import_messages.sass b/app/assets/stylesheets/modules/import_messages.sass index e5666cbcd..cde903b00 100644 --- a/app/assets/stylesheets/modules/import_messages.sass +++ b/app/assets/stylesheets/modules/import_messages.sass @@ -2,4 +2,7 @@ .status_icon padding-right: 20px h1 - padding-bottom: 20px + padding-bottom: 20px + +ul.list-unstyled + padding-bottom: 20px diff --git a/app/assets/stylesheets/typography/_sboiv.sass b/app/assets/stylesheets/typography/_sboiv.sass index f694306c4..f0943f843 100644 --- a/app/assets/stylesheets/typography/_sboiv.sass +++ b/app/assets/stylesheets/typography/_sboiv.sass @@ -52,6 +52,8 @@ &.sb-5x font-size: 5em + &.sb-strong + font-weight: bold .sb-ZDLR:before content: '\e904' @@ -89,7 +91,7 @@ .sb-OAS:before content: '\e90f' -.sb-calendar:before +.sb-calendar:before, .sb-purchase_window:before content: '\e910' .sb-journey_pattern:before diff --git a/app/controllers/api_keys_controller.rb b/app/controllers/api_keys_controller.rb index 9706c5961..a03a67481 100644 --- a/app/controllers/api_keys_controller.rb +++ b/app/controllers/api_keys_controller.rb @@ -31,11 +31,4 @@ class ApiKeysController < ChouetteController def api_key_params params.require(:api_key).permit(:name, :referential_id) end - - def decorate_api_keys(api_keys) - ModelDecorator.decorate( - api_keys, - with: ApiKeyDecorator, - ) - end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 97f5548ae..8bd3da2f9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,7 @@ class ApplicationController < ActionController::Base include PaperTrailSupport include Pundit + include FeatureChecker rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized @@ -40,12 +41,30 @@ class ApplicationController < ActionController::Base end helper_method :current_offer_workbench + def current_workgroup + current_offer_workbench.workgroup + end + helper_method :current_workgroup + def current_functional_scope functional_scope = current_organisation.sso_attributes.try(:[], "functional_scope") if current_organisation JSON.parse(functional_scope) if functional_scope end helper_method :current_functional_scope + def collection_name + self.class.name.split("::").last.gsub('Controller', '').underscore + end + + def decorated_collection + if instance_variable_defined?("@#{collection_name}") + instance_variable_get("@#{collection_name}") + else + nil + end + end + helper_method :decorated_collection + def begin_of_association_chain current_organisation end diff --git a/app/controllers/autocomplete_purchase_windows_controller.rb b/app/controllers/autocomplete_purchase_windows_controller.rb new file mode 100644 index 000000000..70dc5a346 --- /dev/null +++ b/app/controllers/autocomplete_purchase_windows_controller.rb @@ -0,0 +1,12 @@ +class AutocompletePurchaseWindowsController < ChouetteController + respond_to :json, :only => [:index] + + requires_feature :purchase_windows + + include ReferentialSupport + + protected + def collection + @purchase_windows = referential.purchase_windows.search(params[:q]).result.paginate(page: params[:page]) + end +end diff --git a/app/controllers/autocomplete_stop_areas_controller.rb b/app/controllers/autocomplete_stop_areas_controller.rb index 233012028..79154a6e0 100644 --- a/app/controllers/autocomplete_stop_areas_controller.rb +++ b/app/controllers/autocomplete_stop_areas_controller.rb @@ -17,13 +17,20 @@ class AutocompleteStopAreasController < ChouetteController scope = scope.possible_parents if relation_parent? scope = scope.possible_parents if relation_children? end + if search_scope.present? + scope = StopAreaPolicy::Scope.new(current_user, scope).search_scope(search_scope) + end args = [].tap{|arg| 4.times{arg << "%#{params[:q]}%"}} - @stop_areas = scope.where("name ILIKE ? OR city_name ILIKE ? OR registration_number ILIKE ? OR objectid ILIKE ?", *args).limit(50) + @stop_areas = scope.where("unaccent(stop_areas.name) ILIKE unaccent(?) OR unaccent(stop_areas.city_name) ILIKE unaccent(?) OR stop_areas.registration_number ILIKE ? OR stop_areas.objectid ILIKE ?", *args).limit(50) @stop_areas end def target_type? - params.has_key?( :target_type) + params.has_key?(:target_type) + end + + def search_scope + params[:scope] end def relation_parent? diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb index 2ed10a111..75d4cbd09 100644 --- a/app/controllers/calendars_controller.rb +++ b/app/controllers/calendars_controller.rb @@ -1,25 +1,69 @@ class CalendarsController < ChouetteController include PolicyChecker + include TimeTablesHelper + defaults resource_class: Calendar before_action :ransack_contains_date, only: [:index] respond_to :html + respond_to :json, only: :show respond_to :js, only: :index + belongs_to :workgroup + def index index! do - @calendars = ModelDecorator.decorate(@calendars, with: CalendarDecorator) + @calendars = decorate_calendars(@calendars) end end def show show! do - @calendar = @calendar.decorate + @year = params[:year] ? params[:year].to_i : Date.today.cwyear + @calendar = @calendar.decorate(context: { + workgroup: workgroup + }) + end + end + + def month + @date = params['date'] ? Date.parse(params['date']) : Date.today + @calendar = resource + end + + def create + create! do + if @calendar.valid? && has_feature?('application_days_on_calendars') + redirect_to([:edit, @calendar]) + return + end + end + end + + def update + if params[:calendar] + super + else + state = JSON.parse request.raw_post + resource.state_update state + respond_to do |format| + format.json { render json: state, status: state['errors'] ? :unprocessable_entity : :ok } + end end end private + + def decorate_calendars(calendars) + CalendarDecorator.decorate( + calendars, + context: { + workgroup: workgroup + } + ) + end + def calendar_params - permitted_params = [:id, :name, :short_name, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] + permitted_params = [:id, :name, :short_name, :shared, periods_attributes: [:id, :begin, :end, :_destroy], date_values_attributes: [:id, :value, :_destroy]] permitted_params << :shared if policy(Calendar).share? params.require(:calendar).permit(*permitted_params) end @@ -33,25 +77,30 @@ class CalendarsController < ChouetteController end protected + + alias_method :workgroup, :parent + helper_method :workgroup + def resource - @calendar = Calendar.where('organisation_id = ? OR shared = true', current_organisation.id).find_by_id(params[:id]) + @calendar ||= workgroup.calendars.where('(organisation_id = ? OR shared = ?)', current_organisation.id, true).find_by_id(params[:id]) end def build_resource super.tap do |calendar| + calendar.workgroup = workgroup calendar.organisation = current_organisation end end def collection - return @calendars if @calendars - scope = Calendar.where('organisation_id = ? OR shared = ?', current_organisation.id, true) - scope = shared_scope(scope) - @q = scope.ransack(params[:q]) - - calendars = @q.result - calendars = calendars.order(sort_column + ' ' + sort_direction) if sort_column && sort_direction - @calendars = calendars.paginate(page: params[:page]) + @calendars ||= begin + scope = workgroup.calendars.where('(organisation_id = ? OR shared = ?)', current_organisation.id, true) + scope = shared_scope(scope) + @q = scope.ransack(params[:q]) + calendars = @q.result + calendars = calendars.order(sort_column + ' ' + sort_direction) if sort_column && sort_direction + calendars = calendars.paginate(page: params[:page]) + end end def ransack_contains_date @@ -76,4 +125,4 @@ class CalendarsController < ChouetteController scope end -end +end
\ No newline at end of file diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index 931d846c5..4afd12be1 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -47,6 +47,9 @@ class CompaniesController < ChouetteController end end + def resource + super.decorate(context: { referential: line_referential }) + end def resource_url(company = nil) line_referential_company_path(line_referential, company || resource) @@ -61,6 +64,10 @@ class CompaniesController < ChouetteController alias_method :current_referential, :line_referential helper_method :current_referential + def begin_of_association_chain + current_organisation + end + def company_params params.require(:company).permit( :objectid, :object_version, :name, :short_name, :organizational_unit, :operating_department_name, :code, :phone, :fax, :email, :registration_number, :url, :time_zone ) end @@ -75,9 +82,8 @@ class CompaniesController < ChouetteController end def decorate_companies(companies) - ModelDecorator.decorate( + CompanyDecorator.decorate( companies, - with: CompanyDecorator, context: { referential: line_referential } diff --git a/app/controllers/compliance_check_messages_controller.rb b/app/controllers/compliance_check_messages_controller.rb new file mode 100644 index 000000000..7c416584e --- /dev/null +++ b/app/controllers/compliance_check_messages_controller.rb @@ -0,0 +1,34 @@ +class ComplianceCheckMessagesController < ChouetteController + defaults resource_class: ComplianceCheckMessage, collection_name: 'compliance_check_messages', instance_name: 'compliance_check_message' + respond_to :csv + belongs_to :compliance_check_set, :parent_class => ComplianceCheckSet + + + def index + index! do |format| + format.csv { + send_data ComplianceCheckMessageExport.new(compliance_check_messages: collection).to_csv(:col_sep => "\;", :quote_char=>'"', force_quotes: true, server_url: request.base_url) , :filename => "compliance_check_set_errors_#{line_code}_#{Time.now.to_i}.csv" + } + end + end + + protected + def collection + parent.compliance_check_messages.where(compliance_check_resource_id: params[:compliance_check_resource_id]) + end + + def parent + @compliance_check_set ||= ComplianceCheckSet.find(params[:compliance_check_set_id]) + end + + def compliance_check_resource + ComplianceCheckResource.find(params[:compliance_check_resource_id]) + end + + private + + def line_code + Chouette::Line.find_by_objectid("#{compliance_check_resource.reference}").get_objectid.local_id + end + +end diff --git a/app/controllers/compliance_check_sets_controller.rb b/app/controllers/compliance_check_sets_controller.rb index 600c69126..271598428 100644 --- a/app/controllers/compliance_check_sets_controller.rb +++ b/app/controllers/compliance_check_sets_controller.rb @@ -11,9 +11,8 @@ class ComplianceCheckSetsController < ChouetteController scope = self.ransack_period_range(scope: @compliance_check_sets, error_message: t('compliance_check_sets.filters.error_period_filter'), query: :where_created_at_between) @q_for_form = scope.ransack(params[:q]) format.html { - @compliance_check_sets = ModelDecorator.decorate( - @q_for_form.result.order(created_at: :desc), - with: ComplianceCheckSetDecorator + @compliance_check_sets = ComplianceCheckSetDecorator.decorate( + @q_for_form.result.order(created_at: :desc) ) } end @@ -21,9 +20,7 @@ class ComplianceCheckSetsController < ChouetteController def show show! do - @compliance_check_set = @compliance_check_set.decorate(context: { - compliance_check_set: @compliance_check_set - }) + @compliance_check_set = @compliance_check_set.decorate end end @@ -43,18 +40,9 @@ class ComplianceCheckSetsController < ChouetteController def executed_for_html @q_checks_form = @compliance_check_set.compliance_checks.ransack(params[:q]) @compliance_check_set = @compliance_check_set.decorate - compliance_checks = - decorate_compliance_checks( @q_checks_form.result) - .group_by(&:compliance_check_block) + compliance_checks = @q_checks_form.result + .group_by(&:compliance_check_block) @direct_compliance_checks = compliance_checks.delete nil @blocks_to_compliance_checks_map = compliance_checks end - - # Decoration - # ---------- - def decorate_compliance_checks(compliance_checks) - ModelDecorator.decorate( - compliance_checks, - with: ComplianceCheckDecorator) - end end diff --git a/app/controllers/compliance_control_sets_controller.rb b/app/controllers/compliance_control_sets_controller.rb index 2d3d03ad0..8f9251155 100644 --- a/app/controllers/compliance_control_sets_controller.rb +++ b/app/controllers/compliance_control_sets_controller.rb @@ -7,10 +7,8 @@ class ComplianceControlSetsController < ChouetteController def index index! do |format| - scope = self.ransack_period_range(scope: @compliance_control_sets, error_message: t('imports.filters.error_period_filter'), query: :where_updated_at_between) - @q_for_form = scope.ransack(params[:q]) format.html { - @compliance_control_sets = decorate_compliance_control_sets(@q_for_form.result.paginate(page: params[:page], per_page: 30)) + @compliance_control_sets = decorate_compliance_control_sets(@compliance_control_sets) } end end @@ -37,18 +35,20 @@ class ComplianceControlSetsController < ChouetteController private + def collection + scope = self.ransack_period_range(scope: ComplianceControlSet.all, error_message: t('imports.filters.error_period_filter'), query: :where_updated_at_between) + @q_for_form = scope.ransack(params[:q]) + compliance_control_sets = @q_for_form.result + compliance_control_sets = joins_with_associated_objects(compliance_control_sets).order(sort_column + ' ' + sort_direction) if sort_column && sort_direction + @compliance_control_sets = compliance_control_sets.paginate(page: params[:page], per_page: 30) + end + def decorate_compliance_control_sets(compliance_control_sets) - ModelDecorator.decorate( - compliance_control_sets, - with: ComplianceControlSetDecorator - ) + ComplianceControlSetDecorator.decorate(compliance_control_sets) end def decorate_compliance_controls(compliance_controls) - ModelDecorator.decorate( - compliance_controls, - with: ComplianceControlDecorator, - ) + ComplianceControlDecorator.decorate(compliance_controls) end def compliance_control_set_params @@ -64,4 +64,32 @@ class ComplianceControlSetsController < ChouetteController @direct_compliance_controls = compliance_controls.delete nil @blocks_to_compliance_controls_map = compliance_controls end + + def sort_column + case params[:sort] + when 'name' then 'lower(compliance_control_sets.name)' + when 'owner_jdc' then 'lower(organisations.name)' + when 'control_numbers' then 'COUNT(compliance_controls.id)' + else + ComplianceControlSet.column_names.include?(params[:sort]) ? params[:sort] : 'lower(compliance_control_sets.name)' + end + end + + def joins_with_associated_objects(collection) + + # dont know if this is the right way to do it but since we need to join table deoending of the params + # it was to avoid loading associated objects if we don't need them + case params[:sort] + when 'owner_jdc' + collection.joins("LEFT JOIN organisations ON compliance_control_sets.organisation_id = organisations.id") + when 'control_numbers' + collection.joins("LEFT JOIN compliance_controls ON compliance_controls.compliance_control_set_id = compliance_control_sets.id").group(:id) + else + collection + end + end + + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' + end end diff --git a/app/controllers/compliance_controls_controller.rb b/app/controllers/compliance_controls_controller.rb index dfbecaa71..73dc18f59 100644 --- a/app/controllers/compliance_controls_controller.rb +++ b/app/controllers/compliance_controls_controller.rb @@ -8,6 +8,12 @@ class ComplianceControlsController < ChouetteController @sti_subclasses = ComplianceControl.subclasses end + def show + show! do + @compliance_control = @compliance_control.decorate + end + end + def new if params[:sti_class].blank? flash[:notice] = I18n.t("compliance_controls.errors.mandatory_control_type") diff --git a/app/controllers/concerns/activatable.rb b/app/controllers/concerns/activatable.rb new file mode 100644 index 000000000..1a34551a9 --- /dev/null +++ b/app/controllers/concerns/activatable.rb @@ -0,0 +1,11 @@ +module Activatable + extend ActiveSupport::Concern + + %w(activate deactivate).each do |action| + define_method action do + authorize resource, "#{action}?" + resource.send "#{action}!" + redirect_to request.referer || [current_referential, resource] + end + end +end diff --git a/app/controllers/concerns/feature_checker.rb b/app/controllers/concerns/feature_checker.rb new file mode 100644 index 000000000..9ca5ed0a7 --- /dev/null +++ b/app/controllers/concerns/feature_checker.rb @@ -0,0 +1,42 @@ +# Check availability of optional features +# +# In your controller, use : +# +# requires_feature :test +# requires_feature :test, only: [:show] +# +# In your view, use : +# +# has_feature? :test +# +module FeatureChecker + extend ActiveSupport::Concern + + module ClassMethods + def requires_feature(feature, options = {}) + before_action options do + check_feature! feature + end + end + end + + included do + helper_method :has_feature? + end + + protected + + def has_feature?(*features) + features.all? do |feature| + current_organisation.has_feature? feature + end + end + + def check_feature!(*features) + unless has_feature?(*features) + raise NotAuthorizedError, "Feature not autorized" + end + end + + class NotAuthorizedError < StandardError; end +end diff --git a/app/controllers/concerns/ransack_date_filter.rb b/app/controllers/concerns/ransack_date_filter.rb index 0fbde91d3..055c01130 100644 --- a/app/controllers/concerns/ransack_date_filter.rb +++ b/app/controllers/concerns/ransack_date_filter.rb @@ -3,7 +3,15 @@ module RansackDateFilter included do - def set_date_time_params(param_name, klass) + def begin_range_var prefix + "@#{[prefix, "begin_range"].compact.join('_')}" + end + + def end_range_var prefix + "@#{[prefix, "end_range"].compact.join('_')}" + end + + def set_date_time_params(param_name, klass, prefix: nil) start_date = [] end_date = [] @@ -16,26 +24,28 @@ module RansackDateFilter params[:q].delete([param_name]) if klass == DateTime - @begin_range = klass.new(*start_date,0,0,0) rescue nil - @end_range = klass.new(*end_date,23,59,59) rescue nil + instance_variable_set begin_range_var(prefix), klass.new(*start_date,0,0,0) rescue nil + instance_variable_set end_range_var(prefix), klass.new(*end_date,23,59,59) rescue nil else - @begin_range = klass.new(*start_date) rescue nil - @end_range = klass.new(*end_date) rescue nil + instance_variable_set begin_range_var(prefix), klass.new(*start_date) rescue nil + instance_variable_set end_range_var(prefix), klass.new(*end_date) rescue nil end end end # Fake ransack filter def ransack_period_range **options - return options[:scope] unless !!@begin_range && !!@end_range + prefix = options[:prefix] + return options[:scope] unless !!instance_variable_get(begin_range_var(prefix)) && !!instance_variable_get(end_range_var(prefix)) - if @begin_range > @end_range + scope = options[:scope] + if instance_variable_get(begin_range_var(prefix)) > instance_variable_get(end_range_var(prefix)) flash.now[:error] = options[:error_message] else - scope = options[:scope].send options[:query], @begin_range..@end_range + scope = scope.send options[:query], instance_variable_get(begin_range_var(prefix))..instance_variable_get(end_range_var(prefix)) end scope end end -end
\ No newline at end of file +end diff --git a/app/controllers/development_toolbar_controller.rb b/app/controllers/development_toolbar_controller.rb new file mode 100644 index 000000000..20349f7b8 --- /dev/null +++ b/app/controllers/development_toolbar_controller.rb @@ -0,0 +1,11 @@ +class DevelopmentToolbarController < ApplicationController + def update_settings + return unless Rails.application.config.development_toolbar.present? + organisation = current_user.organisation + organisation.features = params[:features].keys.select{|k| params[:features][k] == "true"} + organisation.save + current_user.permissions = params[:permissions].keys.select{|k| params[:permissions][k] == "true"} + current_user.save + redirect_to request.referrer || "/" + end +end diff --git a/app/controllers/group_of_lines_controller.rb b/app/controllers/group_of_lines_controller.rb index 5762108dc..46d9d077f 100644 --- a/app/controllers/group_of_lines_controller.rb +++ b/app/controllers/group_of_lines_controller.rb @@ -42,7 +42,6 @@ class GroupOfLinesController < ChouetteController end end - protected def filtered_group_of_lines_maps @@ -70,6 +69,10 @@ class GroupOfLinesController < ChouetteController alias_method :line_referential, :parent + def begin_of_association_chain + current_organisation + end + private def group_of_line_params diff --git a/app/controllers/import_messages_controller.rb b/app/controllers/import_messages_controller.rb index 6546b25f8..286bb0ce8 100644 --- a/app/controllers/import_messages_controller.rb +++ b/app/controllers/import_messages_controller.rb @@ -9,7 +9,7 @@ class ImportMessagesController < ChouetteController def index index! do |format| format.csv { - send_data ImportMessageExport.new(:import_messages => @import_messages).to_csv(:col_sep => ";") , :filename => "#{File.basename(@import_resource.name)}_#{Time.now.to_i}.csv" + send_data ImportMessageExport.new(:import_messages => @import_messages).to_csv(:col_sep => "\;", :quote_char=>'"', force_quotes: true) , :filename => "import_errors_#{@import_resource.name.gsub('.xml', '')}_#{Time.now.to_i}.csv" } end end diff --git a/app/controllers/import_resources_controller.rb b/app/controllers/import_resources_controller.rb index c83721310..ea78394a1 100644 --- a/app/controllers/import_resources_controller.rb +++ b/app/controllers/import_resources_controller.rb @@ -27,12 +27,6 @@ class ImportResourcesController < ChouetteController private def decorate_import_resources(import_resources) - ImportResourcesDecorator.decorate( - import_resources, - with: ImportResourceDecorator, - context: { - import: @import - } - ) + ImportResourcesDecorator.decorate(import_resources) end end diff --git a/app/controllers/imports_controller.rb b/app/controllers/imports_controller.rb index 46d34efda..7a999d657 100644 --- a/app/controllers/imports_controller.rb +++ b/app/controllers/imports_controller.rb @@ -84,9 +84,8 @@ class ImportsController < ChouetteController end def decorate_imports(imports) - ModelDecorator.decorate( + ImportDecorator.decorate( imports, - with: ImportDecorator, context: { workbench: @workbench } diff --git a/app/controllers/journey_patterns_collections_controller.rb b/app/controllers/journey_patterns_collections_controller.rb index 736fb1441..da567779e 100644 --- a/app/controllers/journey_patterns_collections_controller.rb +++ b/app/controllers/journey_patterns_collections_controller.rb @@ -17,38 +17,53 @@ class JourneyPatternsCollectionsController < ChouetteController alias_method :vehicle_journey, :resource def show - @q = route.journey_patterns.search(params[:q]).result(distinct: true).includes(:stop_points) + @q = route.journey_patterns + if params[:q].present? + ids = @q.search(params[:q]).result(distinct: true).pluck(:id) + @q = @q.where(id: ids) + end + @q = @q.includes(:stop_points) @ppage = 10 @journey_patterns ||= @q.paginate(page: params[:page], per_page: @ppage).order(:name) - - @stop_points_list = [] - route.stop_points.each do |sp| - @stop_points_list << { - :id => sp.stop_area.id, - :route_id => sp.try(:route_id), - :object_id => sp.try(:objectid), - :position => sp.try(:position), - :for_boarding => sp.try(:for_boarding), - :for_alighting => sp.try(:for_alighting), - :name => sp.stop_area.try(:name), - :zip_code => sp.stop_area.try(:zip_code), - :city_name => sp.stop_area.try(:city_name), - :comment => sp.stop_area.try(:comment), - :area_type => sp.stop_area.try(:area_type), - :registration_number => sp.stop_area.try(:registration_number), - :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), - :fare_code => sp.stop_area.try(:fare_code), - :longitude => sp.stop_area.try(:longitude), - :latitude => sp.stop_area.try(:latitude), - :long_lat_type => sp.stop_area.try(:long_lat_type), - :country_code => sp.stop_area.try(:country_code), - :street_name => sp.stop_area.try(:street_name) - } + respond_to do |format| + format.json do + @journey_patterns = @journey_patterns.includes(stop_points: {stop_area: :stop_area_referential}) + end + format.html do + @stop_points_list = [] + route.stop_points.includes(:stop_area).each do |sp| + @stop_points_list << { + :id => sp.stop_area.id, + :route_id => sp.try(:route_id), + :object_id => sp.try(:objectid), + :stop_area_object_id => sp.stop_area.try(:objectid), + :position => sp.try(:position), + :for_boarding => sp.try(:for_boarding), + :for_alighting => sp.try(:for_alighting), + :name => sp.stop_area.try(:name), + :zip_code => sp.stop_area.try(:zip_code), + :city_name => sp.stop_area.try(:city_name), + :country_name => sp.stop_area.try(:country_name), + :time_zone_formatted_offset => sp.stop_area.try(:time_zone_formatted_offset), + :comment => sp.stop_area.try(:comment), + :area_type => sp.stop_area.try(:area_type), + :registration_number => sp.stop_area.try(:registration_number), + :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), + :fare_code => sp.stop_area.try(:fare_code), + :longitude => sp.stop_area.try(:longitude), + :latitude => sp.stop_area.try(:latitude), + :long_lat_type => sp.stop_area.try(:long_lat_type), + :country_code => sp.stop_area.try(:country_code), + :street_name => sp.stop_area.try(:street_name) + } + end + @stop_points_list = @stop_points_list.sort_by {|a| a[:position] } + end end - @stop_points_list = @stop_points_list.sort_by {|a| a[:position] } end def user_permissions + @features = Hash[*current_organisation.features.map{|f| [f, true]}.flatten].to_json policy = policy(:journey_pattern) @perms = %w{create destroy update}.inject({}) do | permissions, action | diff --git a/app/controllers/line_referentials_controller.rb b/app/controllers/line_referentials_controller.rb index 39c2cdb89..03dab3f8f 100644 --- a/app/controllers/line_referentials_controller.rb +++ b/app/controllers/line_referentials_controller.rb @@ -3,6 +3,7 @@ class LineReferentialsController < ChouetteController defaults :resource_class => LineReferential def sync + authorize resource, :synchronize? @sync = resource.line_referential_syncs.build if @sync.save flash[:notice] = t('notice.line_referential_sync.created') diff --git a/app/controllers/lines_controller.rb b/app/controllers/lines_controller.rb index 571c73f4a..27a9bf9be 100644 --- a/app/controllers/lines_controller.rb +++ b/app/controllers/lines_controller.rb @@ -1,6 +1,8 @@ class LinesController < ChouetteController include ApplicationHelper + include Activatable include PolicyChecker + defaults :resource_class => Chouette::Line respond_to :html respond_to :xml @@ -13,9 +15,8 @@ class LinesController < ChouetteController def index @hide_group_of_line = line_referential.group_of_lines.empty? index! do |format| - @lines = ModelDecorator.decorate( + @lines = LineDecorator.decorate( @lines, - with: LineDecorator, context: { line_referential: @line_referential, current_organisation: current_organisation @@ -67,7 +68,6 @@ class LinesController < ChouetteController respond_to do |format| format.json { render :json => filtered_lines_maps} end - end protected @@ -112,6 +112,10 @@ class LinesController < ChouetteController alias_method :current_referential, :line_referential helper_method :current_referential + def begin_of_association_chain + current_organisation + end + def line_params params.require(:line).permit( :transport_mode, diff --git a/app/controllers/merges_controller.rb b/app/controllers/merges_controller.rb new file mode 100644 index 000000000..1ce64ed58 --- /dev/null +++ b/app/controllers/merges_controller.rb @@ -0,0 +1,40 @@ +class MergesController < ChouetteController + # include PolicyChecker + + defaults resource_class: Merge + belongs_to :workbench + + respond_to :html + + before_action :set_mergeable_controllers, only: [:new] + + private + + def set_mergeable_controllers + @mergeable_referentials ||= parent.referentials.mergeable + Rails.logger.debug "Mergeables: #{@mergeable_referentials.inspect}" + end + + def build_resource + super.tap do |merge| + merge.creator = current_user.name + end + end + + # def build_resource + # @import ||= WorkbenchImport.new(*resource_params) do |import| + # import.workbench = parent + # import.creator = current_user.name + # end + # end + + def merge_params + params.require(:merge).permit( + referentials: [] + # :name, + # :file, + # :type, + # :referential_id + ) + end +end diff --git a/app/controllers/networks_controller.rb b/app/controllers/networks_controller.rb index 494d1e69f..1c69b1240 100644 --- a/app/controllers/networks_controller.rb +++ b/app/controllers/networks_controller.rb @@ -71,6 +71,10 @@ class NetworksController < ChouetteController alias_method :current_referential, :line_referential helper_method :current_referential + def begin_of_association_chain + current_organisation + end + def network_params params.require(:network).permit(:objectid, :object_version, :version_date, :description, :name, :registration_number, :source_name, :source_type_name, :source_identifier, :comment ) end @@ -85,9 +89,8 @@ class NetworksController < ChouetteController end def decorate_networks(networks) - ModelDecorator.decorate( + NetworkDecorator.decorate( networks, - with: NetworkDecorator, context: { line_referential: line_referential } diff --git a/app/controllers/purchase_windows_controller.rb b/app/controllers/purchase_windows_controller.rb new file mode 100644 index 000000000..293a7d8e4 --- /dev/null +++ b/app/controllers/purchase_windows_controller.rb @@ -0,0 +1,74 @@ +class PurchaseWindowsController < ChouetteController + include ReferentialSupport + include RansackDateFilter + include PolicyChecker + before_action :ransack_contains_date, only: [:index] + defaults :resource_class => Chouette::PurchaseWindow, collection_name: 'purchase_windows', instance_name: 'purchase_window' + belongs_to :referential + + requires_feature :purchase_windows + + def index + index! do + @purchase_windows = decorate_purchase_windows(@purchase_windows) + end + end + + def show + show! do + @purchase_window = @purchase_window.decorate(context: { + referential: @referential + }) + end + end + + protected + + def create_resource(purchase_window) + purchase_window.referential = @referential + super + end + + private + + def purchase_window_params + params.require(:purchase_window).permit(:id, :name, :color, :referential_id, periods_attributes: [:id, :begin, :end, :_destroy]) + end + + def decorate_purchase_windows(purchase_windows) + PurchaseWindowDecorator.decorate( + purchase_windows, + context: { + referential: @referential + } + ) + end + + def sort_column + Chouette::PurchaseWindow.column_names.include?(params[:sort]) ? params[:sort] : 'name' + end + + def sort_direction + %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' + end + + def collection + return @purchase_windows if @purchase_windows + @q = Chouette::PurchaseWindow.ransack(params[:q]) + + purchase_windows = @q.result + purchase_windows = purchase_windows.order(sort_column + ' ' + sort_direction) if sort_column && sort_direction + @purchase_windows = purchase_windows.paginate(page: params[:page]) + end + + def ransack_contains_date + date =[] + if params[:q] && params[:q]['contains_date(1i)'].present? + ['contains_date(1i)', 'contains_date(2i)', 'contains_date(3i)'].each do |key| + date << params[:q][key].to_i + params[:q].delete(key) + end + params[:q]['contains_date'] = @date = Date.new(*date) rescue nil + end + end +end diff --git a/app/controllers/referential_companies_controller.rb b/app/controllers/referential_companies_controller.rb index ca1ff67db..806a70c8f 100644 --- a/app/controllers/referential_companies_controller.rb +++ b/app/controllers/referential_companies_controller.rb @@ -35,7 +35,8 @@ class ReferentialCompaniesController < ChouetteController def collection scope = referential.line_referential.companies if params[:line_id] - scope = referential.line_referential.lines.find(params[:line_id]).companies + line_scope = referential.line_referential.lines.find(params[:line_id]).companies + scope = line_scope if line_scope.exists? end @q = scope.search(params[:q]) @@ -68,10 +69,13 @@ class ReferentialCompaniesController < ChouetteController %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc' end + def collection_name + "companies" + end + def decorate_companies(companies) - ModelDecorator.decorate( + CompanyDecorator.decorate( companies, - with: CompanyDecorator, context: { referential: referential } diff --git a/app/controllers/referential_lines_controller.rb b/app/controllers/referential_lines_controller.rb index 9e8f5c512..37051faeb 100644 --- a/app/controllers/referential_lines_controller.rb +++ b/app/controllers/referential_lines_controller.rb @@ -28,9 +28,8 @@ class ReferentialLinesController < ChouetteController @routes = @routes.paginate(page: params[:page], per_page: 10) - @routes = ModelDecorator.decorate( + @routes = RouteDecorator.decorate( @routes, - with: RouteDecorator, context: { referential: referential, line: @line diff --git a/app/controllers/referential_networks_controller.rb b/app/controllers/referential_networks_controller.rb index b2d83f953..fe00a99df 100644 --- a/app/controllers/referential_networks_controller.rb +++ b/app/controllers/referential_networks_controller.rb @@ -56,6 +56,10 @@ class ReferentialNetworksController < ChouetteController end end + def collection_name + 'networks' + end + def resource_url(network = nil) referential_network_path(referential, network || resource) end @@ -78,9 +82,8 @@ class ReferentialNetworksController < ChouetteController end def decorate_networks(networks) - ModelDecorator.decorate( + ReferentialNetworkDecorator.decorate( networks, - with: ReferentialNetworkDecorator, context: { referential: referential } diff --git a/app/controllers/referential_vehicle_journeys_controller.rb b/app/controllers/referential_vehicle_journeys_controller.rb new file mode 100644 index 000000000..2ce28a5cc --- /dev/null +++ b/app/controllers/referential_vehicle_journeys_controller.rb @@ -0,0 +1,30 @@ +# +# Browse all VehicleJourneys of the Referential +# +class ReferentialVehicleJourneysController < ChouetteController + include ReferentialSupport + include RansackDateFilter + + before_action only: [:index] { set_date_time_params("purchase_window", Date, prefix: :purchase_window) } + before_action only: [:index] { set_date_time_params("time_table", Date, prefix: :time_table) } + + defaults :resource_class => Chouette::VehicleJourney, collection_name: :vehicle_journeys + + requires_feature :referential_vehicle_journeys + + private + + def collection + @q ||= end_of_association_chain + @q = @q.with_stop_area_ids(params[:q][:stop_area_ids]) if params[:q] && params[:q][:stop_area_ids] + @q = ransack_period_range(scope: @q, error_message: t('vehicle_journeys.errors.purchase_window'), query: :in_purchase_window, prefix: :purchase_window) + @q = ransack_period_range(scope: @q, error_message: t('vehicle_journeys.errors.time_table'), query: :with_matching_timetable, prefix: :time_table) + @q = @q.ransack(params[:q]) + @vehicle_journeys ||= @q.result.order(:published_journey_name).includes(:vehicle_journey_at_stops).paginate page: params[:page], per_page: params[:per_page] || 10 + @all_companies = Chouette::Company.where("id IN (#{@referential.vehicle_journeys.select(:company_id).to_sql})").distinct + @all_stop_areas = Chouette::StopArea.where("id IN (#{@referential.vehicle_journeys.joins(:stop_areas).select("stop_areas.id").to_sql})").distinct + stop_area_ids = params[:q].try(:[], :stop_area_ids).try(:select, &:present?) + @filters_stop_areas = Chouette::StopArea.find(stop_area_ids) if stop_area_ids.present? && stop_area_ids.size <= 2 + end + +end diff --git a/app/controllers/referentials_controller.rb b/app/controllers/referentials_controller.rb index ee1236912..0ed3f75dd 100644 --- a/app/controllers/referentials_controller.rb +++ b/app/controllers/referentials_controller.rb @@ -8,16 +8,21 @@ class ReferentialsController < ChouetteController def new new! do - build_referenial + build_referential end end def create - create! do |format| - build_referenial - - if !!@referential.created_from_id - format.html { redirect_to workbench_path(@referential.workbench) } + create! do |success, failure| + success.html do + if @referential.created_from_id.present? + flash[:notice] = t('notice.referentials.duplicate') + end + redirect_to workbench_path(@referential.workbench) + end + failure.html do + Rails.logger.info "Can't create Referential : #{@referential.errors.inspect}" + render :new end end end @@ -27,9 +32,8 @@ class ReferentialsController < ChouetteController show! do |format| @referential = @referential.decorate(context: { current_workbench_id: params[:current_workbench_id] } ) @reflines = lines_collection.paginate(page: params[:page], per_page: 10) - @reflines = ModelDecorator.decorate( + @reflines = ReferentialLineDecorator.decorate( @reflines, - with: ReferentialLineDecorator, context: { referential: referential, current_organisation: current_organisation @@ -60,8 +64,8 @@ class ReferentialsController < ChouetteController def validate ComplianceControlSetCopyWorker.perform_async(params[:compliance_control_set], params[:id]) - flash[:notice] = I18n.t("referentials.operation_in_progress") - redirect_to(referential_path) + flash[:notice] = t('notice.referentials.validate') + redirect_to workbench_compliance_check_sets_path(referential.workbench_id) end def destroy @@ -75,6 +79,7 @@ class ReferentialsController < ChouetteController referential.archive! redirect_to workbench_path(referential.workbench_id), notice: t('notice.referential.archived') end + def unarchive if referential.unarchive! flash[:notice] = t('notice.referential.unarchived') @@ -92,7 +97,7 @@ class ReferentialsController < ChouetteController helper_method :current_referential def resource - @referential ||= current_organisation.find_referential(params[:id]) + @referential ||= current_organisation.find_referential(params[:id]).decorate end def collection @@ -132,7 +137,7 @@ class ReferentialsController < ChouetteController super end - def build_referenial + def build_referential if params[:from] source_referential = Referential.find(params[:from]) @referential = Referential.new_from(source_referential, current_functional_scope) diff --git a/app/controllers/routes_controller.rb b/app/controllers/routes_controller.rb index 79f49143a..af5a9a91b 100644 --- a/app/controllers/routes_controller.rb +++ b/app/controllers/routes_controller.rb @@ -47,10 +47,7 @@ class RoutesController < ChouetteController line: @line }) - @route_sp = ModelDecorator.decorate( - @route_sp, - with: StopPointDecorator - ) + @route_sp = StopPointDecorator.decorate(@route_sp) end end diff --git a/app/controllers/routing_constraint_zones_controller.rb b/app/controllers/routing_constraint_zones_controller.rb index a72b288b8..47df211d0 100644 --- a/app/controllers/routing_constraint_zones_controller.rb +++ b/app/controllers/routing_constraint_zones_controller.rb @@ -13,9 +13,8 @@ class RoutingConstraintZonesController < ChouetteController def index index! do |format| - @routing_constraint_zones = ModelDecorator.decorate( + @routing_constraint_zones = RoutingConstraintZoneDecorator.decorate( @routing_constraint_zones, - with: RoutingConstraintZoneDecorator, context: { referential: referential, line: parent diff --git a/app/controllers/snapshots_controller.rb b/app/controllers/snapshots_controller.rb new file mode 100644 index 000000000..e453b4965 --- /dev/null +++ b/app/controllers/snapshots_controller.rb @@ -0,0 +1,14 @@ +class SnapshotsController < ApplicationController + if Rails.env.development? || Rails.env.test? + layout :which_layout + def show + tpl = params[:snap] + tpl = tpl.gsub Rails.root.to_s, '' + render file: tpl + end + + def which_layout + "snapshots/#{params[:layout] || "default"}" + end + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb new file mode 100644 index 000000000..e38a92982 --- /dev/null +++ b/app/controllers/statuses_controller.rb @@ -0,0 +1,20 @@ +class StatusesController < ChouetteController + respond_to :json + + def index + + status = { + referentials_blocked: Referential.blocked.count, + imports_blocked: Import.blocked.count, + compliance_check_sets_blocked: ComplianceCheckSet.blocked.count + } + status[:status] = global_status status + render json: status.to_json + end + + private + + def global_status status + status.values.all?(&:zero?) ? 'ok' : 'ko' + end +end diff --git a/app/controllers/stop_area_referentials_controller.rb b/app/controllers/stop_area_referentials_controller.rb index 85541230d..f2d375e49 100644 --- a/app/controllers/stop_area_referentials_controller.rb +++ b/app/controllers/stop_area_referentials_controller.rb @@ -2,6 +2,7 @@ class StopAreaReferentialsController < ChouetteController defaults :resource_class => StopAreaReferential def sync + authorize resource, :synchronize? @sync = resource.stop_area_referential_syncs.build if @sync.save flash[:notice] = t('notice.stop_area_referential_sync.created') diff --git a/app/controllers/stop_areas_controller.rb b/app/controllers/stop_areas_controller.rb index 133518324..41a1a8c6d 100644 --- a/app/controllers/stop_areas_controller.rb +++ b/app/controllers/stop_areas_controller.rb @@ -1,5 +1,6 @@ class StopAreasController < ChouetteController include ApplicationHelper + include Activatable defaults :resource_class => Chouette::StopArea @@ -13,10 +14,12 @@ class StopAreasController < ChouetteController respond_to :html, :kml, :xml, :json respond_to :js, :only => :index - # def complete - # @stop_areas = line.stop_areas - # render :layout => false - # end + def autocomplete + scope = stop_area_referential.stop_areas.where(deleted_at: nil) + args = [].tap{|arg| 4.times{arg << "%#{params[:q]}%"}} + @stop_areas = scope.where("unaccent(name) ILIKE unaccent(?) OR unaccent(city_name) ILIKE unaccent(?) OR registration_number ILIKE ? OR objectid ILIKE ?", *args).limit(50) + @stop_areas + end def select_parent @stop_area = stop_area @@ -54,10 +57,7 @@ class StopAreasController < ChouetteController redirect_to params.merge(:page => 1) end - @stop_areas = ModelDecorator.decorate( - @stop_areas, - with: StopAreaDecorator - ) + @stop_areas = StopAreaDecorator.decorate(@stop_areas) } end end @@ -95,9 +95,8 @@ class StopAreasController < ChouetteController def edit authorize stop_area edit! do - stop_area.position ||= stop_area.default_position map.editable = true - end + end end def destroy @@ -107,7 +106,6 @@ class StopAreasController < ChouetteController def update authorize stop_area - stop_area.position ||= stop_area.default_position map.editable = true update! @@ -154,6 +152,10 @@ class StopAreasController < ChouetteController end end + def begin_of_association_chain + current_organisation + end + private def sort_column @@ -171,7 +173,37 @@ class StopAreasController < ChouetteController helper_method :current_referential def stop_area_params - params.require(:stop_area).permit( :routing_stop_ids, :routing_line_ids, :children_ids, :stop_area_type, :parent_id, :objectid, :object_version, :name, :comment, :area_type, :registration_number, :nearest_topic_name, :fare_code, :longitude, :latitude, :long_lat_type, :country_code, :street_name, :zip_code, :city_name, :mobility_restricted_suitability, :stairs_availability, :lift_availability, :int_user_needs, :coordinates, :url, :time_zone ) + params.require(:stop_area).permit( + :area_type, + :children_ids, + :city_name, + :comment, + :coordinates, + :country_code, + :fare_code, + :int_user_needs, + :latitude, + :lift_availability, + :long_lat_type, + :longitude, + :mobility_restricted_suitability, + :name, + :nearest_topic_name, + :object_version, + :objectid, + :parent_id, + :registration_number, + :routing_line_ids, + :routing_stop_ids, + :stairs_availability, + :street_name, + :time_zone, + :url, + :waiting_time, + :zip_code, + :kind, + localized_names: Chouette::StopArea::AVAILABLE_LOCALIZATIONS + ) end end diff --git a/app/controllers/time_tables_controller.rb b/app/controllers/time_tables_controller.rb index a0fa168f0..0dcadad1e 100644 --- a/app/controllers/time_tables_controller.rb +++ b/app/controllers/time_tables_controller.rb @@ -36,7 +36,6 @@ class TimeTablesController < ChouetteController def create tt_params = time_table_params if tt_params[:calendar_id] && tt_params[:calendar_id] != "" - %i(monday tuesday wednesday thursday friday saturday sunday).map { |d| tt_params[d] = true } calendar = Calendar.find(tt_params[:calendar_id]) tt_params[:calendar_id] = nil if tt_params.has_key?(:dates_attributes) || tt_params.has_key?(:periods_attributes) end @@ -45,6 +44,7 @@ class TimeTablesController < ChouetteController @time_table = created_from ? created_from.duplicate : Chouette::TimeTable.new(tt_params) if calendar + @time_table.int_day_types = calendar.int_day_types calendar.dates.each_with_index do |date, i| @time_table.dates << Chouette::TimeTableDate.new(date: date, position: i, in_out: true) end @@ -167,9 +167,8 @@ class TimeTablesController < ChouetteController end def decorate_time_tables(time_tables) - ModelDecorator.decorate( + TimeTableDecorator.decorate( time_tables, - with: TimeTableDecorator, context: { referential: @referential } diff --git a/app/controllers/vehicle_journeys_controller.rb b/app/controllers/vehicle_journeys_controller.rb index c941aeae4..e031e4952 100644 --- a/app/controllers/vehicle_journeys_controller.rb +++ b/app/controllers/vehicle_journeys_controller.rb @@ -40,43 +40,26 @@ class VehicleJourneysController < ChouetteController end def index - @stop_points_list = [] - route.stop_points.each do |sp| - @stop_points_list << { - :id => sp.stop_area.id, - :route_id => sp.try(:route_id), - :object_id => sp.try(:objectid), - :position => sp.try(:position), - :for_boarding => sp.try(:for_boarding), - :for_alighting => sp.try(:for_alighting), - :name => sp.stop_area.try(:name), - :zip_code => sp.stop_area.try(:zip_code), - :city_name => sp.stop_area.try(:city_name), - :comment => sp.stop_area.try(:comment), - :area_type => sp.stop_area.try(:area_type), - :registration_number => sp.stop_area.try(:registration_number), - :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), - :fare_code => sp.stop_area.try(:fare_code), - :longitude => sp.stop_area.try(:longitude), - :latitude => sp.stop_area.try(:latitude), - :long_lat_type => sp.stop_area.try(:long_lat_type), - :country_code => sp.stop_area.try(:country_code), - :street_name => sp.stop_area.try(:street_name) - } - end - - @transport_mode = route.line['transport_mode'] - @transport_submode = route.line['transport_submode'] - - if params[:jp] - @jp_origin = Chouette::JourneyPattern.find_by(objectid: params[:jp]) - @jp_origin_stop_points = @jp_origin.stop_points - end - - index! do + index! do |format| if collection.out_of_bounds? redirect_to params.merge(:page => 1) end + format.json do + @vehicle_journeys = @vehicle_journeys.includes({stop_points: :stop_area}) + end + format.html do + load_missions + load_custom_fields + @stop_points_list = map_stop_points(route.stop_points) + @return_stop_points_list = map_stop_points(route.opposite_route&.stop_points) if has_feature?(:vehicle_journeys_return_route) + @transport_mode = route.line['transport_mode'] + @transport_submode = route.line['transport_submode'] + + if params[:jp] + @jp_origin = Chouette::JourneyPattern.find_by(objectid: params[:jp]) + @jp_origin_stop_points = @jp_origin.stop_points + end + end end end @@ -92,13 +75,15 @@ class VehicleJourneysController < ChouetteController scope = maybe_filter_by_departure_time(scope) scope = maybe_filter_out_journeys_with_time_tables(scope) - @q = scope.search filtered_ransack_params + @vehicle_journeys ||= begin + @q = scope.search filtered_ransack_params - @ppage = 20 - @vehicle_journeys = @q.result.paginate(:page => params[:page], :per_page => @ppage) - @footnotes = route.line.footnotes.to_json - @matrix = resource_class.matrix(@vehicle_journeys) - @vehicle_journeys + @ppage = 20 + vehicle_journeys = @q.result.paginate(:page => params[:page], :per_page => @ppage) + @footnotes = route.line.footnotes.to_json + @matrix = resource_class.matrix(vehicle_journeys) + vehicle_journeys + end end def maybe_filter_by_departure_time(scope) @@ -159,6 +144,7 @@ class VehicleJourneysController < ChouetteController end def user_permissions + @features = Hash[*current_organisation.features.map{|f| [f, true]}.flatten].to_json policy = policy(:vehicle_journey) @perms = %w{create destroy update}.inject({}) do | permissions, action | @@ -167,6 +153,69 @@ class VehicleJourneysController < ChouetteController end private + def load_custom_fields + @custom_fields = current_workgroup.custom_fields_definitions + end + + def map_stop_points points + (points&.includes(:stop_area) || []).map do |sp| + { + :id => sp.stop_area.id, + :route_id => sp.try(:route_id), + :object_id => sp.try(:objectid), + :area_object_id => sp.stop_area.try(:objectid), + :position => sp.try(:position), + :for_boarding => sp.try(:for_boarding), + :for_alighting => sp.try(:for_alighting), + :name => sp.stop_area.try(:name), + :time_zone_offset => sp.stop_area.try(:time_zone_offset), + :time_zone_formatted_offset => sp.stop_area.try(:time_zone_formatted_offset), + :zip_code => sp.stop_area.try(:zip_code), + :city_name => sp.stop_area.try(:city_name), + :comment => sp.stop_area.try(:comment), + :area_type => sp.stop_area.try(:area_type), + :area_type_i18n => I18n.t(sp.stop_area.try(:area_type), scope: 'area_types.label'), + :area_kind => sp.stop_area.try(:kind), + :stop_area_id => sp.stop_area_id, + :registration_number => sp.stop_area.try(:registration_number), + :nearest_topic_name => sp.stop_area.try(:nearest_topic_name), + :fare_code => sp.stop_area.try(:fare_code), + :longitude => sp.stop_area.try(:longitude), + :latitude => sp.stop_area.try(:latitude), + :long_lat_type => sp.stop_area.try(:long_lat_type), + :country_code => sp.stop_area.try(:country_code), + :country_name => sp.stop_area.try(:country_name), + :street_name => sp.stop_area.try(:street_name) + } + end + end + + def load_missions + @all_missions = route.journey_patterns.count > 10 ? [] : route.journey_patterns.map do |item| + { + id: item.id, + "data-item": { + id: item.id, + name: item.name, + published_name: item.published_name, + object_id: item.objectid, + short_id: item.get_objectid.short_id, + full_schedule: item.full_schedule?, + costs: item.costs, + stop_area_short_descriptions: item.stop_areas.map do |stop| + { + stop_area_short_description: { + id: stop.id, + name: stop.name, + object_id: item.objectid + } + } + end + }.to_json, + text: "<strong>#{item.published_name} - #{item.get_objectid.short_id}</strong><br/><small>#{item.registration_number}</small>" + } + end + end def vehicle_journey_params params.require(:vehicle_journey).permit( { footnote_ids: [] }, diff --git a/app/controllers/workbench_outputs_controller.rb b/app/controllers/workbench_outputs_controller.rb new file mode 100644 index 000000000..67ed7569e --- /dev/null +++ b/app/controllers/workbench_outputs_controller.rb @@ -0,0 +1,9 @@ +class WorkbenchOutputsController < ChouetteController + respond_to :html, only: [:show] + defaults resource_class: Workbench + + def show + @workbench = current_organisation.workbenches.find params[:workbench_id] + @workbench_merges = @workbench.merges.order("created_at desc").paginate(page: params[:page], per_page: 10) + end +end diff --git a/app/controllers/workbenches_controller.rb b/app/controllers/workbenches_controller.rb index b2dac9e67..2a71fe811 100644 --- a/app/controllers/workbenches_controller.rb +++ b/app/controllers/workbenches_controller.rb @@ -18,9 +18,8 @@ class WorkbenchesController < ChouetteController @q_for_form = scope.ransack(params[:q]) @q_for_result = scope.ransack(ransack_params) @wbench_refs = sort_result(@q_for_result.result).paginate(page: params[:page], per_page: 30) - @wbench_refs = ModelDecorator.decorate( + @wbench_refs = ReferentialDecorator.decorate( @wbench_refs, - with: ReferentialDecorator, context: { current_workbench_id: params[:id] } diff --git a/app/decorators/api_key_decorator.rb b/app/decorators/api_key_decorator.rb deleted file mode 100644 index def3a6a01..000000000 --- a/app/decorators/api_key_decorator.rb +++ /dev/null @@ -1,30 +0,0 @@ -class ApiKeyDecorator < Draper::Decorator - decorates Api::V1::ApiKey - delegate_all - - - def action_links - links = [] - - links << Link.new( - content: h.t('api_keys.actions.show'), - href: h.organisation_api_key_path(object), - ) - - links << Link.new( - content: h.t('api_keys.actions.edit'), - href: h.edit_organisation_api_key_path(object), - ) - - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.organisation_api_key_path(object), - method: :delete, - data: { confirm: h.t('api_keys.actions.destroy_confirm') } - ) - end - - links - end -end diff --git a/app/decorators/calendar_decorator.rb b/app/decorators/calendar_decorator.rb index 37e2cfe80..4c6088e8e 100644 --- a/app/decorators/calendar_decorator.rb +++ b/app/decorators/calendar_decorator.rb @@ -1,18 +1,13 @@ -class CalendarDecorator < Draper::Decorator - delegate_all +class CalendarDecorator < AF83::Decorator + decorates Calendar + set_scope { context[:workgroup] } + create_action_link - def action_links - links = [] - - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.calendar_path(object), - method: :delete, - data: { confirm: h.t('calendars.actions.destroy_confirm') } - ) + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link + instance_decorator.edit_action_link + instance_decorator.destroy_action_link do |l| + l.data {{ confirm: h.t('calendars.actions.destroy_confirm') }} end - - links end end diff --git a/app/decorators/company_decorator.rb b/app/decorators/company_decorator.rb index 9416c73ae..5580e0d4a 100644 --- a/app/decorators/company_decorator.rb +++ b/app/decorators/company_decorator.rb @@ -1,52 +1,22 @@ -class CompanyDecorator < Draper::Decorator +class CompanyDecorator < AF83::Decorator decorates Chouette::Company - delegate_all + set_scope { context[:referential] } - def self.collection_decorator_class - PaginatingDecorator + create_action_link do |l| + l.content { h.t('companies.actions.new') } end - def linecount - object.lines.count - end - - # Requires: - # context: { - # referential: - # } - def action_links - links = [] + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - if h.policy(Chouette::Company).create? - links << Link.new( - content: h.t('companies.actions.new'), - href: h.new_line_referential_company_path(context[:referential]) - ) + instance_decorator.edit_action_link do |l| + l.content {|l| l.action == "show" ? h.t('actions.edit') : h.t('companies.actions.edit') } end - if h.policy(object).update? - links << Link.new( - content: h.t('companies.actions.edit'), - href: h.edit_line_referential_company_path( - context[:referential], - object - ) - ) + instance_decorator.destroy_action_link do |l| + l.content { h.destroy_link_content('companies.actions.destroy') } + l.data {{ confirm: h.t('companies.actions.destroy_confirm') }} end - - if h.policy(object).destroy? - links << Link.new( - content: t('companies.actions.destroy'), - href: h.line_referential_company_path( - context[:referential], - object - ), - method: :delete, - data: { confirm: h.t('companies.actions.destroy_confirm') } - ) - end - - links end end diff --git a/app/decorators/compliance_check_decorator.rb b/app/decorators/compliance_check_decorator.rb deleted file mode 100644 index 5431f5796..000000000 --- a/app/decorators/compliance_check_decorator.rb +++ /dev/null @@ -1,8 +0,0 @@ -class ComplianceCheckDecorator < Draper::Decorator - delegate_all - - def action_links - [] - end - -end diff --git a/app/decorators/compliance_check_set_decorator.rb b/app/decorators/compliance_check_set_decorator.rb index 096596b19..334f39e88 100644 --- a/app/decorators/compliance_check_set_decorator.rb +++ b/app/decorators/compliance_check_set_decorator.rb @@ -1,24 +1,15 @@ -class ComplianceCheckSetDecorator < Draper::Decorator - delegate_all +class ComplianceCheckSetDecorator < AF83::Decorator + decorates ComplianceCheckSet - def action_links - links = [] - - links << Link.new( - content: h.destroy_link_content, - href: h.workbench_compliance_check_sets_path(object.id), - method: :delete, - data: {confirm: h.t('imports.actions.destroy_confirm')} - ) - - links + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link end - def lines_status + define_instance_method :lines_status do object.compliance_check_resources.where(status: :OK, resource_type: :line).count end - def lines_in_compliance_check_set + define_instance_method :lines_in_compliance_check_set do object.compliance_check_resources.where(resource_type: :line).count end diff --git a/app/decorators/compliance_control_decorator.rb b/app/decorators/compliance_control_decorator.rb index f56e80417..fd2dbd9ce 100644 --- a/app/decorators/compliance_control_decorator.rb +++ b/app/decorators/compliance_control_decorator.rb @@ -1,30 +1,35 @@ -class ComplianceControlDecorator < Draper::Decorator - delegate_all +class ComplianceControlDecorator < AF83::Decorator + decorates ComplianceControl - def action_links - policy = h.policy(object) - links = [] + set_scope { object.compliance_control_set } - links << Link.new( - content: h.t('compliance_control_sets.actions.show'), - href: h.compliance_control_set_compliance_control_path(object.compliance_control_set.id, object.id) - ) - - if policy.edit? - links << Link.new( - content: h.t('compliance_controls.actions.edit'), - href: h.edit_compliance_control_set_compliance_control_path(object.compliance_control_set.id, object.id) - ) + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link do |l| + l.content h.t('compliance_control_sets.actions.show') + l.href do + h.compliance_control_set_compliance_control_path( + object.compliance_control_set.id, + object.id + ) + end end - if policy.destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.compliance_control_set_compliance_control_path(object.compliance_control_set.id, object.id), - method: :delete, - data: { confirm: h.t('compliance_controls.actions.destroy_confirm') } - ) + instance_decorator.edit_action_link + + instance_decorator.destroy_action_link do |l| + l.data confirm: h.t('compliance_controls.actions.destroy_confirm') end - links + end + + define_instance_class_method :predicate do + object_class.predicate + end + + define_instance_class_method :prerequisite do + object_class.prerequisite + end + + define_instance_class_method :dynamic_attributes do + object_class.dynamic_attributes end end diff --git a/app/decorators/compliance_control_set_decorator.rb b/app/decorators/compliance_control_set_decorator.rb index 73d65d54a..b16a06886 100644 --- a/app/decorators/compliance_control_set_decorator.rb +++ b/app/decorators/compliance_control_set_decorator.rb @@ -1,35 +1,26 @@ -class ComplianceControlSetDecorator < Draper::Decorator - delegate_all +class ComplianceControlSetDecorator < AF83::Decorator + decorates ComplianceControlSet - def action_links - policy = h.policy(object) - links = [] + create_action_link do |l| + l.content t('compliance_control_sets.actions.new') + end - if policy.edit? - links << Link.new( - content: h.t('compliance_control_sets.actions.edit'), - href: h.edit_compliance_control_set_path(object.id) - ) - end + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - if policy.clone? - links << Link.new( - content: h.t('actions.clone'), - href: h.clone_compliance_control_set_path(object.id) - ) + instance_decorator.edit_action_link do |l| + l.content t('compliance_control_sets.actions.edit') end - if policy.destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.compliance_control_set_path(object.id), - method: :delete, - data: { confirm: h.t('compliance_control_sets.actions.destroy_confirm') } - ) + instance_decorator.action_link policy: :clone, secondary: :show do |l| + l.content t('actions.clone') + l.href { h.clone_compliance_control_set_path(object.id) } end - links + instance_decorator.destroy_action_link do |l| + l.content h.destroy_link_content + l.href { h.compliance_control_set_path(object.id) } + l.data confirm: h.t('compliance_control_sets.actions.destroy_confirm') + end end - end - diff --git a/app/decorators/import_decorator.rb b/app/decorators/import_decorator.rb index e748f830d..1964365ae 100644 --- a/app/decorators/import_decorator.rb +++ b/app/decorators/import_decorator.rb @@ -1,9 +1,9 @@ -class ImportDecorator < Draper::Decorator +class ImportDecorator < AF83::Decorator decorates Import - delegate_all + set_scope { context[:workbench] } - def import_status_css_class + define_instance_method :import_status_css_class do cls ='' cls = 'overheaded-success' if object.status == 'successful' cls = 'overheaded-warning' if object.status == 'warning' @@ -11,36 +11,16 @@ class ImportDecorator < Draper::Decorator cls end - def action_links - policy = h.policy(object) - links = [] - - links << Link.new( - content: h.t('imports.actions.show'), - href: h.workbench_import_path( - context[:workbench], - object - ) - ) + create_action_link do |l| + l.content t('imports.actions.new') + end - links << Link.new( - content: h.t('imports.actions.download'), - href: object.file.url - ) + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - if policy.destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.workbench_import_path( - context[:workbench], - object - ), - method: :delete, - data: { confirm: h.t('imports.actions.destroy_confirm') } - ) + instance_decorator.action_link secondary: :show do |l| + l.content t('imports.actions.download') + l.href { object.file.url } end - - links end - end diff --git a/app/decorators/import_resource_decorator.rb b/app/decorators/import_resource_decorator.rb deleted file mode 100644 index 9bfd1f757..000000000 --- a/app/decorators/import_resource_decorator.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ImportResourceDecorator < Draper::Decorator - decorates ImportResource - - delegate_all - - def action_links - links = [] - end - -end diff --git a/app/decorators/import_resources_decorator.rb b/app/decorators/import_resources_decorator.rb index 2b1a25ef9..88a8057cf 100644 --- a/app/decorators/import_resources_decorator.rb +++ b/app/decorators/import_resources_decorator.rb @@ -1,4 +1,4 @@ -class ImportResourcesDecorator < ModelDecorator +class ImportResourcesDecorator < AF83::Decorator delegate :where def lines_imported diff --git a/app/decorators/line_decorator.rb b/app/decorators/line_decorator.rb index ede670cbd..0e7b6b9ae 100644 --- a/app/decorators/line_decorator.rb +++ b/app/decorators/line_decorator.rb @@ -1,55 +1,70 @@ -class LineDecorator < Draper::Decorator +class LineDecorator < AF83::Decorator decorates Chouette::Line - delegate_all - - # Requires: - # context: { - # line_referential: , - # current_organisation: - # } - def action_links - links = [] - - links << Link.new( - content: h.t('lines.actions.show_network'), - href: [context[:line_referential], object.network] - ) - - links << Link.new( - content: h.t('lines.actions.show_company'), - href: [context[:line_referential], object.company] - ) - - if h.policy(Chouette::Line).create? && - context[:line_referential].organisations.include?( - context[:current_organisation] - ) - links << Link.new( - content: h.t('lines.actions.edit'), - href: h.edit_line_referential_line_path(context[:line_referential], object.id) - ) + set_scope { context[:line_referential] } + + create_action_link do |l| + l.content t('lines.actions.new') + end + + with_instance_decorator do |instance_decorator| + ### primary (and secondary) can be + ### - a single action + ### - an array of actions + ### - a boolean + + instance_decorator.show_action_link do |l| + l.content t('lines.actions.show') + end + + instance_decorator.action_link secondary: :show do |l| + l.content t('lines.actions.show_network') + l.href { [scope, object.network] } + l.disabled { object.network.nil? } + end + + instance_decorator.action_link secondary: :show do |l| + l.content t('lines.actions.show_company') + l.href { [scope, object.company] } + l.disabled { object.company.nil? } end - if h.policy(Chouette::Line).create? && - context[:line_referential].organisations.include?( - context[:current_organisation] - ) - links << Link.new( - content: h.t('lines.actions.new'), - href: h.new_line_referential_line_path(context[:line_referential]) - ) + can_edit_line = ->(){ h.policy(Chouette::Line).create? && context[:line_referential].organisations.include?(context[:current_organisation]) } + + instance_decorator.with_condition can_edit_line do + edit_action_link do |l| + l.content {|l| l.primary? ? h.t('actions.edit') : h.t('lines.actions.edit') } + end + + action_link on: :index, secondary: :index do |l| + l.content t('lines.actions.new') + l.href { h.new_line_referential_line_path(context[:line_referential]) } + end end - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content('lines.actions.destroy'), - href: h.line_referential_line_path(context[:line_referential], object), - method: :delete, - data: {confirm: h.t('lines.actions.destroy_confirm')} - ) + ### the option :policy will automatically check for the corresponding method + ### on the object's policy + + instance_decorator.action_link policy: :deactivate, secondary: :show, footer: :index do |l| + l.content { h.deactivate_link_content('lines.actions.deactivate') } + l.href { h.deactivate_line_referential_line_path(context[:line_referential], object) } + l.method :put + l.data confirm: h.t('lines.actions.deactivate_confirm') + l.add_class "delete-action" end - links + instance_decorator.action_link policy: :activate, secondary: :show, footer: :index do |l| + l.content { h.activate_link_content('lines.actions.activate') } + l.href { h.activate_line_referential_line_path(context[:line_referential], object) } + l.method :put + l.data confirm: h.t('lines.actions.activate_confirm') + l.add_class "delete-action" + end + + instance_decorator.destroy_action_link do |l| + l.content { h.destroy_link_content('lines.actions.destroy') } + l.data confirm: h.t('lines.actions.destroy_confirm') + l.add_class "delete-action" + end end end diff --git a/app/decorators/network_decorator.rb b/app/decorators/network_decorator.rb index 1f62fe512..ea0f73dc2 100644 --- a/app/decorators/network_decorator.rb +++ b/app/decorators/network_decorator.rb @@ -1,44 +1,32 @@ -class NetworkDecorator < Draper::Decorator +class NetworkDecorator < AF83::Decorator decorates Chouette::Network - delegate_all - - # Requires: + set_scope { context[:line_referential] } + # Action links require: # context: { # line_referential: , # } - def action_links - links = [] - if h.policy(Chouette::Network).create? - links << Link.new( - content: h.t('networks.actions.new'), - href: h.new_line_referential_network_path(context[:line_referential]) - ) - end + create_action_link do |l| + l.content t('networks.actions.new') + end + + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - if h.policy(object).update? - links << Link.new( - content: h.t('networks.actions.edit'), - href: h.edit_line_referential_network_path( + instance_decorator.action_link secondary: true, policy: :edit do |l| + l.content t('networks.actions.edit') + l.href do + h.edit_line_referential_network_path( context[:line_referential], object ) - ) + end end - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content('networks.actions.destroy'), - href: h.line_referential_network_path( - context[:line_referential], - object - ), - method: :delete, - data: { confirm: t('networks.actions.destroy_confirm') } - ) + instance_decorator.destroy_action_link do |l| + l.content h.destroy_link_content('networks.actions.destroy') + l.data confirm: h.t('networks.actions.destroy_confirm') end - - links end end diff --git a/app/decorators/purchase_window_decorator.rb b/app/decorators/purchase_window_decorator.rb new file mode 100644 index 000000000..9b58577b2 --- /dev/null +++ b/app/decorators/purchase_window_decorator.rb @@ -0,0 +1,28 @@ +class PurchaseWindowDecorator < AF83::Decorator + decorates Chouette::PurchaseWindow + + set_scope { context[:referential] } + + create_action_link do |l| + l.content t('purchase_windows.actions.new') + end + + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link do |l| + l.content t('purchase_windows.actions.show') + end + + instance_decorator.edit_action_link + + instance_decorator.destroy_action_link do |l| + l.data confirm: h.t('purchase_windows.actions.destroy_confirm') + end + end + + define_instance_method :bounding_dates do + unless object.date_ranges.empty? + object.date_ranges.map(&:min).min..object.date_ranges.map(&:max).max + end + end + +end diff --git a/app/decorators/referential_decorator.rb b/app/decorators/referential_decorator.rb index 4103790aa..3132cbf92 100644 --- a/app/decorators/referential_decorator.rb +++ b/app/decorators/referential_decorator.rb @@ -1,66 +1,60 @@ -class ReferentialDecorator < Draper::Decorator - delegate_all +class ReferentialDecorator < AF83::Decorator + decorates Referential - def action_links - policy = h.policy(object) - links = [ - Link.new( - content: h.t('time_tables.index.title'), - href: h.referential_time_tables_path(object) - ) - ] + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link + instance_decorator.edit_action_link - if policy.clone? - links << Link.new( - content: h.t('actions.clone'), - href: h.new_referential_path(from: object.id, current_workbench_id: context[:current_workbench_id]) - ) + instance_decorator.action_link feature: :referential_vehicle_journeys, secondary: :show, on: :show do |l| + l.content t('referential_vehicle_journeys.index.title') + l.href { h.referential_vehicle_journeys_path(object) } end - if policy.validate? - links << Link.new( - content: h.t('actions.validate'), - href: h.referential_select_compliance_control_set_path(object.id) - ) + instance_decorator.action_link feature: :purchase_windows, secondary: :show, on: :show do |l| + l.content t('purchase_windows.index.title') + l.href { h.referential_purchase_windows_path(object) } end - if policy.archive? - links << Link.new( - content: h.t('actions.archive'), - href: h.archive_referential_path(object.id), - method: :put - ) + instance_decorator.action_link secondary: :show do |l| + l.content t('time_tables.index.title') + l.href { h.referential_time_tables_path(object) } end - if policy.unarchive? - links << Link.new( - content: h.t('actions.unarchive'), - href: h.unarchive_referential_path(object.id), - method: :put - ) + instance_decorator.action_link policy: :clone, secondary: :show do |l| + l.content t('actions.clone') + l.href { h.new_referential_path(from: object.id, current_workbench_id: context[:current_workbench_id]) } end - if policy.edit? - links << HTMLElement.new( - :button, - 'Purger', - type: 'button', - data: { - toggle: 'modal', - target: '#purgeModal' - } - ) + instance_decorator.action_link policy: :validate, secondary: :show do |l| + l.content t('actions.validate') + l.href { h.referential_select_compliance_control_set_path(object.id) } end - if policy.destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.referential_path(object), - method: :delete, - data: { confirm: h.t('referentials.actions.destroy_confirm') } - ) + instance_decorator.action_link policy: :archive, secondary: :show do |l| + l.content t('actions.archive') + l.href { h.archive_referential_path(object.id) } + l.method :put end - links + instance_decorator.action_link policy: :unarchive, secondary: :show, on: :show do |l| + l.content t('actions.unarchive') + l.href { h.unarchive_referential_path(object.id) } + l.method :put + end + + instance_decorator.action_link policy: :edit, secondary: :show, on: :show do |l| + l.content 'Purger' + l.href '#' + l.type 'button' + l.data {{ + toggle: 'modal', + target: '#purgeModal' + }} + end + + instance_decorator.destroy_action_link do |l| + l.href { h.referential_path(object) } + l.data {{ confirm: h.t('referentials.actions.destroy_confirm') }} + end end end diff --git a/app/decorators/referential_line_decorator.rb b/app/decorators/referential_line_decorator.rb index 55acf7ed9..3ac846d76 100644 --- a/app/decorators/referential_line_decorator.rb +++ b/app/decorators/referential_line_decorator.rb @@ -1,64 +1,43 @@ -class ReferentialLineDecorator < Draper::Decorator +class ReferentialLineDecorator < AF83::Decorator decorates Chouette::Line - delegate_all + set_scope { context[:referential] } - # Requires: + # Action links require: # context: { # referential: , # current_organisation: # } - def action_links - links = [] - links << Link.new( - content: Chouette::Line.human_attribute_name(:footnotes), - href: h.referential_line_footnotes_path(context[:referential], object) - ) + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - links << Link.new( - content: h.t('routing_constraint_zones.index.title'), - href: h.referential_line_routing_constraint_zones_path( - context[:referential], - object - ) - ) - - if h.policy(Chouette::Line).create? && - context[:referential].organisation == context[:current_organisation] - links << Link.new( - content: h.t('actions.new'), - href: h.new_referential_line_path(context[:referential]) - ) - end - - if h.policy(object).update? - links << Link.new( - content: h.t('actions.edit'), - href: h.edit_referential_line_path(context[:referential], object) - ) + instance_decorator.action_link secondary: true do |l| + l.content Chouette::Line.human_attribute_name(:footnotes) + l.href { h.referential_line_footnotes_path(context[:referential], object) } end - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content('actions.destroy'), - href: h.referential_line_path(context[:referential], object), - method: :delete, - data: { confirm: t('lines.actions.destroy_confirm') } - ) - end - - if !object.hub_restricted? || - (object.hub_restricted? && object.routes.size < 2) - if h.policy(Chouette::Route).create? && - context[:referential].organisation == context[:current_organisation] - links << Link.new( - content: h.t('routes.actions.new'), - href: h.new_referential_line_route_path(context[:referential], object) + instance_decorator.action_link secondary: true do |l| + l.content h.t('routing_constraint_zones.index.title') + l.href do + h.referential_line_routing_constraint_zones_path( + scope, + object ) end end - links + instance_decorator.action_link( + if: ->() { + (!object.hub_restricted? || + (object.hub_restricted? && object.routes.size < 2)) && + (h.policy(Chouette::Route).create? && + context[:referential].organisation == context[:current_organisation]) + }, + secondary: true + ) do |l| + l.content h.t('routes.actions.new') + l.href { h.new_referential_line_route_path(scope, object) } + end end end diff --git a/app/decorators/referential_network_decorator.rb b/app/decorators/referential_network_decorator.rb index 1260a38cb..c508452c0 100644 --- a/app/decorators/referential_network_decorator.rb +++ b/app/decorators/referential_network_decorator.rb @@ -1,38 +1,27 @@ -class ReferentialNetworkDecorator < Draper::Decorator +class ReferentialNetworkDecorator < AF83::Decorator decorates Chouette::Network - delegate_all + set_scope { context[:referential] } -# Requires: -# context: { -# referential: , -# } -def action_links - links = [] + # Action links require: + # context: { + # referential: , + # } - if h.policy(Chouette::Network).create? - links << Link.new( - content: h.t('networks.actions.new'), - href: h.new_referential_network_path(context[:referential]) - ) + create_action_link do |l| + l.content t('networks.actions.new') end - if h.policy(object).update? - links << Link.new( - content: h.t('networks.actions.edit'), - href: h.edit_referential_network_path(context[:referential], object) - ) - end + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content('networks.actions.destroy'), - href: h.referential_network_path(context[:referential], object), - method: :delete, - data: { confirm: t('networks.actions.destroy_confirm') } - ) - end + instance_decorator.edit_action_link do |l| + l.content t('networks.actions.edit') + end - links + instance_decorator.destroy_action_link do |l| + l.content h.destroy_link_content('networks.actions.destroy') + l.data confirm: h.t('networks.actions.destroy_confirm') + end end -end
\ No newline at end of file +end diff --git a/app/decorators/route_decorator.rb b/app/decorators/route_decorator.rb index ec7f0d6aa..fa6367924 100644 --- a/app/decorators/route_decorator.rb +++ b/app/decorators/route_decorator.rb @@ -1,75 +1,78 @@ -class RouteDecorator < Draper::Decorator +class RouteDecorator < AF83::Decorator decorates Chouette::Route - delegate_all - - # Requires: + # Action links require: # context: { # referential: , # line: # } - def action_links - links = [] - if object.stop_points.any? - links << Link.new( - content: h.t('journey_patterns.actions.index'), - href: [ + set_scope { [context[:referential], context[:line]] } + + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link + + instance_decorator.edit_action_link + + instance_decorator.action_link( + if: ->() { object.stop_points.any? }, + secondary: :show + ) do |l| + l.content h.t('journey_patterns.actions.index') + l.href do + [ context[:referential], context[:line], object, :journey_patterns_collection ] - ) + end end - if object.journey_patterns.present? - links << Link.new( - content: h.t('vehicle_journeys.actions.index'), - href: [ + instance_decorator.action_link( + if: ->() { object.journey_patterns.present? }, + secondary: :show + ) do |l| + l.content h.t('vehicle_journeys.actions.index') + l.href do + [ context[:referential], context[:line], object, :vehicle_journeys ] - ) + end end - links << Link.new( - content: h.t('vehicle_journey_exports.new.title'), - href: h.referential_line_route_vehicle_journey_exports_path( - context[:referential], - context[:line], - object, - format: :zip - ) - ) - - if h.policy(object).duplicate? - links << Link.new( - content: h.t('routes.duplicate.title'), - href: h.duplicate_referential_line_route_path( + instance_decorator.action_link secondary: :show do |l| + l.content h.t('vehicle_journey_exports.new.title') + l.href do + h.referential_line_route_vehicle_journey_exports_path( context[:referential], context[:line], - object - ), - method: :post - ) + object, + format: :zip + ) + end end - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.referential_line_route_path( + instance_decorator.action_link( + secondary: :show, + policy: :duplicate + ) do |l| + l.content h.t('routes.duplicate.title') + l.method :post + l.href do + h.duplicate_referential_line_route_path( context[:referential], context[:line], object - ), - method: :delete, - data: { confirm: h.t('routes.actions.destroy_confirm') } - ) + ) + end end - links + instance_decorator.destroy_action_link do |l| + l.data confirm: h.t('routes.actions.destroy_confirm') + end end end diff --git a/app/decorators/routing_constraint_zone_decorator.rb b/app/decorators/routing_constraint_zone_decorator.rb index 0b438a554..de73068be 100644 --- a/app/decorators/routing_constraint_zone_decorator.rb +++ b/app/decorators/routing_constraint_zone_decorator.rb @@ -1,42 +1,27 @@ -class RoutingConstraintZoneDecorator < Draper::Decorator +class RoutingConstraintZoneDecorator < AF83::Decorator decorates Chouette::RoutingConstraintZone - delegate_all + set_scope { [context[:referential], context[:line]] } - # Requires: + # Action links require: # context: { # referential: , # line: # } - def action_links - links = [] - if h.policy(object).update? - links << Link.new( - content: h.t('actions.edit'), - href: h.edit_referential_line_routing_constraint_zone_path( - context[:referential], - context[:line], - object - ) - ) - end + create_action_link( + if: ->() { + h.policy(Chouette::RoutingConstraintZone).create? && + context[:referential].organisation == h.current_organisation + } + ) - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.referential_line_routing_constraint_zone_path( - context[:referential], - context[:line], - object - ), - method: :delete, - data: { - confirm: h.t('routing_constraint_zones.actions.destroy_confirm') - } - ) - end + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link + instance_decorator.edit_action_link - links + instance_decorator.destroy_action_link do |l| + l.data confirm: h.t('routing_constraint_zones.actions.destroy_confirm') + end end end diff --git a/app/decorators/stop_area_decorator.rb b/app/decorators/stop_area_decorator.rb index 4e777292d..525681971 100644 --- a/app/decorators/stop_area_decorator.rb +++ b/app/decorators/stop_area_decorator.rb @@ -1,43 +1,54 @@ -class StopAreaDecorator < Draper::Decorator +class StopAreaDecorator < AF83::Decorator decorates Chouette::StopArea - delegate_all + create_action_link do |l| + l.content t('stop_areas.actions.new') + l.href { h.new_stop_area_referential_stop_area_path } + end + + with_instance_decorator do |instance_decorator| + set_scope { object.stop_area_referential } + instance_decorator.show_action_link - def action_links(stop_area = nil) - links = [] - stop_area ||= object + instance_decorator.edit_action_link do |l| + l.content h.t('stop_areas.actions.edit') + end - if h.policy(Chouette::StopArea).new? - links << Link.new( - content: h.t('stop_areas.actions.new'), - href: h.new_stop_area_referential_stop_area_path( - stop_area.stop_area_referential + instance_decorator.action_link policy: :deactivate, secondary: true do |l| + l.content h.deactivate_link_content('stop_areas.actions.deactivate') + l.href do + h.deactivate_stop_area_referential_stop_area_path( + object.stop_area_referential, + object ) - ) + end + l.method :put + l.data confirm: h.t('stop_areas.actions.deactivate_confirm') + l.add_class 'delete-action' end - if h.policy(stop_area).update? - links << Link.new( - content: h.t('stop_areas.actions.edit'), - href: h.edit_stop_area_referential_stop_area_path( - stop_area.stop_area_referential, - stop_area + instance_decorator.action_link policy: :activate, secondary: true do |l| + l.content h.activate_link_content('stop_areas.actions.activate') + l.href do + h.activate_stop_area_referential_stop_area_path( + object.stop_area_referential, + object ) - ) + end + l.method :put + l.data confirm: h.t('stop_areas.actions.activate_confirm') + l.add_class 'delete-action' end - if h.policy(stop_area).destroy? - links << Link.new( - content: h.destroy_link_content('stop_areas.actions.destroy'), - href: h.stop_area_referential_stop_area_path( - stop_area.stop_area_referential, - stop_area - ), - method: :delete, - data: { confirm: t('stop_areas.actions.destroy_confirm') } - ) + instance_decorator.destroy_action_link do |l| + l.content h.destroy_link_content('stop_areas.actions.destroy') + l.data confirm: h.t('stop_areas.actions.destroy_confirm') end + end - links + define_instance_method :waiting_time_text do + return '-' if [nil, 0].include? waiting_time + h.t('stop_areas.waiting_time_format', value: waiting_time) end + end diff --git a/app/decorators/stop_point_decorator.rb b/app/decorators/stop_point_decorator.rb index 196d6d490..e777e2b56 100644 --- a/app/decorators/stop_point_decorator.rb +++ b/app/decorators/stop_point_decorator.rb @@ -1,9 +1,35 @@ -class StopPointDecorator < StopAreaDecorator +class StopPointDecorator < AF83::Decorator decorates Chouette::StopPoint - delegate_all + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link do |l| + l.href do + h.referential_stop_area_path( + object.referential, + object.stop_area + ) + end + end - def action_links - super(object.stop_area) + instance_decorator.edit_action_link do |l| + l.content h.t('stop_points.actions.edit') + l.href do + h.edit_stop_area_referential_stop_area_path( + object.stop_area.stop_area_referential, + object.stop_area + ) + end + end + + instance_decorator.destroy_action_link do |l| + l.content h.destroy_link_content('stop_points.actions.destroy') + l.href do + h.referential_stop_area_path( + object.referential, + object.stop_area + ) + end + l.data confirm: h.t('stop_points.actions.destroy_confirm') + end end end diff --git a/app/decorators/time_table_decorator.rb b/app/decorators/time_table_decorator.rb index c6eeac176..e4f9d7dbc 100644 --- a/app/decorators/time_table_decorator.rb +++ b/app/decorators/time_table_decorator.rb @@ -1,55 +1,53 @@ -class TimeTableDecorator < Draper::Decorator +class TimeTableDecorator < AF83::Decorator decorates Chouette::TimeTable - delegate_all + create_action_link if: ->{ h.policy(Chouette::TimeTable).create? && context[:referential].organisation == h.current_organisation } do |l| + l.href { h.new_referential_time_table_path(context[:referential]) } + end + + with_instance_decorator do |instance_decorator| + instance_decorator.show_action_link do |l| + l.href { [context[:referential], object] } + end - # Requires: - # context: { - # referential: , - # } - def action_links - links = [] + instance_decorator.edit_action_link do |l| + l.href { [:edit, context[:referential], object] } + end - if object.calendar - links << Link.new( - content: h.t('actions.actualize'), - href: h.actualize_referential_time_table_path( + instance_decorator.action_link policy: :actualize, if: ->{ object.calendar }, secondary: true do |l| + l.content t('actions.actualize') + l.href do + h.actualize_referential_time_table_path( context[:referential], object - ), - method: :post - ) + ) + end + l.method :post end - if h.policy(object).edit? - links << Link.new( - content: h.t('actions.combine'), - href: h.new_referential_time_table_time_table_combination_path( + instance_decorator.action_link policy: :edit, secondary: true do |l| + l.content t('actions.combine') + l.href do + h.new_referential_time_table_time_table_combination_path( context[:referential], object ) - ) + end end - if h.policy(object).duplicate? - links << Link.new( - content: h.t('actions.duplicate'), - href: h.duplicate_referential_time_table_path( + instance_decorator.action_link policy: :duplicate, secondary: true do |l| + l.content t('actions.duplicate') + l.href do + h.duplicate_referential_time_table_path( context[:referential], object ) - ) + end end - if h.policy(object).destroy? - links << Link.new( - content: h.destroy_link_content, - href: h.referential_time_table_path(context[:referential], object), - method: :delete, - data: { confirm: h.t('time_tables.actions.destroy_confirm') } - ) + instance_decorator.destroy_action_link do |l| + l.href { h.referential_time_table_path(context[:referential], object) } + l.data {{ confirm: h.t('time_tables.actions.destroy_confirm') }} end - - links end end diff --git a/app/errors/table_lock_timeout_error.rb b/app/errors/table_lock_timeout_error.rb new file mode 100644 index 000000000..102f3a4a0 --- /dev/null +++ b/app/errors/table_lock_timeout_error.rb @@ -0,0 +1 @@ +class TableLockTimeoutError < ActiveRecord::StatementInvalid; end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 124604cd9..0058c210d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,6 +14,11 @@ module ApplicationHelper def page_header_title(object) # Unwrap from decorator, we want to know the object model name object = object.object if object.try(:object) + + if Referential === object + return object.full_name + end + local = "#{object.model_name.name.underscore.pluralize}.#{params[:action]}.title" if object.try(:name) t(local, name: object.name || object.id) @@ -23,12 +28,18 @@ module ApplicationHelper end def page_header_meta(object) - info = t('last_update', time: l(object.updated_at, format: :short)) - if object.try(:versions) - author = object.versions.try(:last).try(:whodunnit) || t('default_whodunnit') - info = "#{info} <br/> #{t('whodunnit', author: author)}" + out = "" + display = true + display = policy(object).synchronize? if policy(object).respond_to?(:synchronize?) rescue false + if display + info = t('last_update', time: l(object.updated_at, format: :short)) + if object.try(:versions) + author = object.versions.try(:last).try(:whodunnit) || t('default_whodunnit') + info = "#{info} <br/> #{t('whodunnit', author: author)}" + end + out += content_tag :div, info.html_safe, class: 'small last-update' end - content_tag :div, info.html_safe, class: 'small' + out.html_safe end def page_header_content_for(object) diff --git a/app/helpers/breadcrumb_helper.rb b/app/helpers/breadcrumb_helper.rb index 3da119247..2175e0be4 100644 --- a/app/helpers/breadcrumb_helper.rb +++ b/app/helpers/breadcrumb_helper.rb @@ -1,5 +1,12 @@ module BreadcrumbHelper def breadcrumb_name(object, prop='name') - "#{object.class.model_name.human} #{object.public_send(prop)}".truncate(50) + name = + if prop == 'name' && object.respond_to?(:full_name) + object.full_name + else + "#{object.class.model_name.human} #{object.public_send(prop)}" + end + + name.truncate(40) end end diff --git a/app/helpers/common_helpers.rb b/app/helpers/common_helpers.rb deleted file mode 100644 index 29cabddac..000000000 --- a/app/helpers/common_helpers.rb +++ /dev/null @@ -1,26 +0,0 @@ - -module CommonHelpers - # TODO: Needs refactoring, but does not seem to be under test - # so let us refactor this **after** test coverage. - def access_links_pairs(access_links) - hpairs = Hash.new - pairs = Array.new - access_links.each do |link| - key = pair_key(link) - pair = nil - if (hpairs.has_key? key) - pair = hpairs[key] - else - pair = AccessLinkPair.new - pairs << pair - hpairs[key] = pair - end - if (link.link_orientation_type == "access_point_to_stop_area") - pair.from_access_point = link - else - pair.to_access_point = link - end - end - pairs - end -end diff --git a/app/helpers/compliance_check_resources_helper.rb b/app/helpers/compliance_check_resources_helper.rb new file mode 100644 index 000000000..95cabed88 --- /dev/null +++ b/app/helpers/compliance_check_resources_helper.rb @@ -0,0 +1,12 @@ +module ComplianceCheckResourcesHelper + + # Comlpiance Check Resources statuses helper + def compliance_check_resource_status(status) + cls = '' + cls = 'success' if status == 'OK' + cls = 'warning' if status == 'WARNING' + cls = 'danger' if %w[ERROR IGNORED].include? status + + content_tag :span, '', class: "fa fa-circle text-#{cls}" + end +end diff --git a/app/helpers/compliance_check_sets_helper.rb b/app/helpers/compliance_check_sets_helper.rb index b255aee63..690bee80e 100644 --- a/app/helpers/compliance_check_sets_helper.rb +++ b/app/helpers/compliance_check_sets_helper.rb @@ -20,9 +20,9 @@ module ComplianceCheckSetsHelper content_tag :span, '', class: "fa fa-clock-o" else cls ='' - cls = 'success' if status == 'OK' - cls = 'warning' if status == 'WARNING' - cls = 'danger' if %w[ERROR IGNORED].include? status + cls = 'success' if status == 'successful' + cls = 'warning' if status == 'warning' + cls = 'danger' if %w[failed aborted canceled].include? status content_tag :span, '', class: "fa fa-circle text-#{cls}" end diff --git a/app/helpers/compliance_control_sets_helper.rb b/app/helpers/compliance_control_sets_helper.rb index 57e6d9608..448d5c008 100644 --- a/app/helpers/compliance_control_sets_helper.rb +++ b/app/helpers/compliance_control_sets_helper.rb @@ -100,7 +100,8 @@ module ComplianceControlSetsHelper ], sortable: true, cls: 'table has-filter has-search', - model: ComplianceControl + model: ComplianceControl, + action: :index end metas = content_tag :div, I18n.t('compliance_control_blocks.metas.control', count: compliance_controls.count), class: 'pull-right' table + metas diff --git a/app/helpers/imports_helper.rb b/app/helpers/imports_helper.rb index 1c4549e50..140660153 100644 --- a/app/helpers/imports_helper.rb +++ b/app/helpers/imports_helper.rb @@ -15,6 +15,20 @@ module ImportsHelper end end + # Compliance check set messages + def bootstrap_class_for_message_criticity message_criticity + case message_criticity + when "error" + "alert alert-danger" + when "warning" + "alert alert-warning" + when "info" + "alert alert-info" + else + message_criticity.to_s + end + end + ############################## # TO CLEAN!!! ############################## diff --git a/app/helpers/links_helper.rb b/app/helpers/links_helper.rb index 4fb7a797d..088415dc3 100644 --- a/app/helpers/links_helper.rb +++ b/app/helpers/links_helper.rb @@ -1,5 +1,18 @@ module LinksHelper + def custom_link_content(translation_key, klass, extra_class: nil) + klass = ["fa", "fa-#{klass}", "mr-xs", extra_class].compact.join(" ") + content_tag(:span, nil, class: klass) + t(translation_key) + end + def destroy_link_content(translation_key = 'actions.destroy') - content_tag(:span, nil, class: 'fa fa-trash mr-xs') + t(translation_key) + custom_link_content translation_key, 'trash' + end + + def deactivate_link_content(translation_key = 'actions.deactivate') + custom_link_content translation_key, 'power-off', extra_class: "text-danger" + end + + def activate_link_content(translation_key = 'actions.activate') + custom_link_content translation_key, 'power-off', extra_class: "text-success" end end diff --git a/app/helpers/newapplication_helper.rb b/app/helpers/newapplication_helper.rb index df19113db..6600a03f7 100644 --- a/app/helpers/newapplication_helper.rb +++ b/app/helpers/newapplication_helper.rb @@ -1,3 +1,4 @@ +# coding: utf-8 module NewapplicationHelper # Table Builder @@ -147,7 +148,7 @@ module NewapplicationHelper content_tag :li, link_to(t("actions.#{action}"), polymorph_url) end elsif action == :archive - unless item.archived? + unless item.referential_read_only? content_tag :li, link_to(t("actions.#{action}"), polymorph_url, method: :put) end elsif action == :unarchive diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 02eec39dc..9b6042377 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -25,7 +25,7 @@ module PaginationHelper if collection.total_pages > 1 links = content_tag :div, '', class: 'page_links' do - will_paginate collection, container: false, page_links: false, previous_label: '', next_label: '' + will_paginate collection, container: false, page_links: false, previous_label: '', next_label: '', param_name: collection.try(:pagination_param_name) end content_tag :div, pinfos.concat(links).html_safe, class: "pagination #{cls}" diff --git a/app/helpers/referentials_helper.rb b/app/helpers/referentials_helper.rb index 01e5a5879..8251377aa 100644 --- a/app/helpers/referentials_helper.rb +++ b/app/helpers/referentials_helper.rb @@ -10,4 +10,9 @@ module ReferentialsHelper t('true') end end + + def referential_overview referential + service = ReferentialOverview.new referential, self + render partial: "referentials/overview", locals: {referential: referential, overview: service} + end end diff --git a/app/helpers/routes_helper.rb b/app/helpers/routes_helper.rb index 4bffa99d4..61714a066 100644 --- a/app/helpers/routes_helper.rb +++ b/app/helpers/routes_helper.rb @@ -19,13 +19,15 @@ module RoutesHelper css end - def route_json_for_edit(route) - route.stop_points.includes(:stop_area).order(:position).map do |stop_point| + def route_json_for_edit(route, serialize: true) + data = route.stop_points.includes(:stop_area).order(:position).map do |stop_point| stop_area_attributes = stop_point.stop_area.attributes.slice("name","city_name", "zip_code", "registration_number", "longitude", "latitude", "area_type", "comment") stop_area_attributes["short_name"] = truncate(stop_area_attributes["name"], :length => 30) || "" stop_point_attributes = stop_point.attributes.slice("for_boarding","for_alighting") stop_area_attributes.merge(stop_point_attributes).merge(stoppoint_id: stop_point.id, stoparea_id: stop_point.stop_area.id).merge(user_objectid: stop_point.stop_area.user_objectid) - end.to_json + end + data = data.to_json if serialize + data end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index f4976ea53..be70d974d 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -8,4 +8,19 @@ module SearchHelper link_to name, params.deep_merge("q" => search,:page => 1), html_options end + def filter_item_class q, key + active = false + if q.present? && q[key].present? + val = q[key] + if val.is_a?(Array) + active = val.any? &:present? + elsif val.is_a?(Hash) + active = val.values.any? &:present? + else + active = true + end + end + active ? 'active' : 'inactive' + end + end diff --git a/app/helpers/stop_areas_helper.rb b/app/helpers/stop_areas_helper.rb index 3e04fac7d..05ae042f5 100644 --- a/app/helpers/stop_areas_helper.rb +++ b/app/helpers/stop_areas_helper.rb @@ -11,6 +11,10 @@ module StopAreasHelper ( "<img src='#{stop_area_picture_url(stop_area)}'/>" + " <span style='height:25px; line-height:25px; margin-left: 5px; '>" + name + " <small style='height:25px; line-height:25px; margin-left: 10px; color: #555;'>" + localization + "</small></span>").html_safe end + def label_for_country country, txt=nil + "#{txt} <span title='#{ISO3166::Country[country]&.translation(I18n.locale)}' class='flag-icon flag-icon-#{country}'></span>".html_safe + end + def genealogical_title return t("stop_areas.genealogical.genealogical_routing") if @stop_area.stop_area_type == 'itl' t("stop_areas.genealogical.genealogical") @@ -33,12 +37,10 @@ module StopAreasHelper @stop_area.stop_area_type == 'stop_place' || @stop_area.stop_area_type == 'commercial_stop_point' end - def pair_key(access_link) "#{access_link.access_point.id}-#{access_link.stop_area.id}" end - def geo_data(sa, sar) if sa.long_lat_type.nil? content_tag :span, '-' diff --git a/app/helpers/table_builder_helper.rb b/app/helpers/table_builder_helper.rb index 37f01ce0d..2068dd23c 100644 --- a/app/helpers/table_builder_helper.rb +++ b/app/helpers/table_builder_helper.rb @@ -86,18 +86,36 @@ module TableBuilderHelper overhead: [], # Possibility to override the result of collection.model - model: nil + model: nil, + + #overrides the params[:action] value + action: nil ) content_tag :table, - thead(collection, columns, sortable, selectable, links.any?, overhead, model || collection.model) + - tbody(collection, columns, selectable, links, overhead), + thead(collection, columns, sortable, selectable, links.any?, overhead, model || collection.model, action || params[:action]) + + tbody(collection, columns, selectable, links, overhead, model, action || params[:action]), class: cls end + def self.item_row_class_name collection, model=nil + model_name = model&.name + + model_name ||= + if collection.respond_to?(:model) + collection.model.name + elsif collection.respond_to?(:first) + collection.first.class.name + else + "item" + end + + model_name.split("::").last.parameterize + end + private - def thead(collection, columns, sortable, selectable, has_links, overhead, model ) + def thead(collection, columns, sortable, selectable, has_links, overhead, model, action) content_tag :thead do # Inserts overhead content if any specified over_head = '' @@ -176,7 +194,9 @@ module TableBuilderHelper end # Inserts a blank column for the gear menu - if has_links || collection.last.try(:action_links).try(:any?) + last_item = collection.last + action_links = last_item && last_item.respond_to?(:action_links) && (last_item&.action_links&.is_a?(AF83::Decorator::ActionLinks) ? last_item.action_links(action) : last_item.action_links) + if has_links || action_links.try(:any?) hcont << content_tag(:th, '') end @@ -187,86 +207,94 @@ module TableBuilderHelper end end - def tbody(collection, columns, selectable, links, overhead) - content_tag :tbody do - collection.map do |item| + def tr item, columns, selectable, links, overhead, model_name, action + klass = "#{model_name}-#{item.id}" + content_tag :tr, class: klass do + bcont = [] + if selectable + disabled = selectable.respond_to?(:call) && !selectable.call(item) + bcont << content_tag( + :td, + checkbox(id_name: item.try(:id), value: item.try(:id), disabled: disabled) + ) + end - content_tag :tr do - bcont = [] + columns.each do |column| + value = column.value(item) - if selectable - bcont << content_tag( - :td, - checkbox(id_name: item.try(:id), value: item.try(:id)) - ) - end + if column.linkable? + path = column.link_to(item) + link = link_to(value, path) - columns.each do |column| - value = column.value(item) + if overhead.empty? + bcont << content_tag(:td, link, title: 'Voir') - if column.linkable? - path = column.link_to(item) - link = link_to(value, path) + else + i = columns.index(column) - if overhead.empty? - bcont << content_tag(:td, link, title: 'Voir') + if overhead[i].blank? + if (i > 0) && (overhead[i - 1][:width] > 1) + clsArrayAlt = overhead[i - 1][:cls].split + + bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArrayAlt)) else - i = columns.index(column) + bcont << content_tag(:td, link, title: 'Voir') + end - if overhead[i].blank? - if (i > 0) && (overhead[i - 1][:width] > 1) - clsArrayAlt = overhead[i - 1][:cls].split + else + clsArray = overhead[columns.index(column)][:cls].split - bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArrayAlt)) + bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArray)) + end + end - else - bcont << content_tag(:td, link, title: 'Voir') - end + else + if overhead.empty? + bcont << content_tag(:td, value) - else - clsArray = overhead[columns.index(column)][:cls].split + else + i = columns.index(column) - bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArray)) - end - end + if overhead[i].blank? + if (i > 0) && (overhead[i - 1][:width] > 1) + clsArrayAlt = overhead[i - 1][:cls].split - else - if overhead.empty? - bcont << content_tag(:td, value) + bcont << content_tag(:td, value, class: td_cls(clsArrayAlt)) else - i = columns.index(column) + bcont << content_tag(:td, value) + end - if overhead[i].blank? - if (i > 0) && (overhead[i - 1][:width] > 1) - clsArrayAlt = overhead[i - 1][:cls].split + else + clsArray = overhead[i][:cls].split - bcont << content_tag(:td, value, class: td_cls(clsArrayAlt)) + bcont << content_tag(:td, value, class: td_cls(clsArray)) + end + end + end + end - else - bcont << content_tag(:td, value) - end + action_links = item && item.respond_to?(:action_links) && (item.action_links.is_a?(AF83::Decorator::ActionLinks) ? item.action_links(action) : item.action_links) - else - clsArray = overhead[i][:cls].split + if links.any? || action_links.try(:any?) + bcont << content_tag( + :td, + build_links(item, links, action), + class: 'actions' + ) + end - bcont << content_tag(:td, value, class: td_cls(clsArray)) - end - end - end - end + bcont.join.html_safe + end + end - if links.any? || item.try(:action_links).try(:any?) - bcont << content_tag( - :td, - build_links(item, links), - class: 'actions' - ) - end + def tbody(collection, columns, selectable, links, overhead, model = nil, action) + model_name = TableBuilderHelper.item_row_class_name collection, model - bcont.join.html_safe - end + content_tag :tbody do + collection.map do |item| + tr item, columns, selectable, links, overhead, model_name, action end.join.html_safe end end @@ -279,7 +307,7 @@ module TableBuilderHelper end end - def build_links(item, links) + def build_links(item, links, action) trigger = content_tag( :div, class: 'btn dropdown-toggle', @@ -288,13 +316,26 @@ module TableBuilderHelper content_tag :span, '', class: 'fa fa-cog' end - menu = content_tag :ul, class: 'dropdown-menu' do - ( - CustomLinks.new(item, pundit_user, links, referential).links + - item.action_links.select { |link| link.is_a?(Link) } - ).map do |link| - gear_menu_link(link) - end.join.html_safe + action_links = item.action_links + if action_links.is_a?(AF83::Decorator::ActionLinks) + menu = content_tag :div, class: 'dropdown-menu' do + item.action_links(action).grouped_by(:primary, :secondary, :footer).map do |group, _links| + if _links.any? + content_tag :ul, class: group do + _links.map{|link| gear_menu_link(link)}.join.html_safe + end + end + end.join.html_safe + end + else + menu = content_tag :ul, class: 'dropdown-menu' do + ( + CustomLinks.new(item, pundit_user, links, referential, workgroup).links + + action_links.select { |link| link.is_a?(Link) } + ).map do |link| + gear_menu_link(link) + end.join.html_safe + end end content_tag :div, trigger + menu, class: 'btn-group' @@ -341,14 +382,19 @@ module TableBuilderHelper end end - def checkbox(id_name:, value:) + def checkbox(id_name:, value:, disabled: false) content_tag :div, '', class: 'checkbox' do - check_box_tag(id_name, value).concat( + check_box_tag(id_name, value, nil, disabled: disabled).concat( content_tag(:label, '', for: id_name) ) end end + def gear_menu_link(link) + klass = [] + klass << link.extra_class if link.extra_class + klass << 'delete-action' if link.method == :delete + klass << 'disabled' if link.disabled content_tag( :li, link_to( @@ -358,7 +404,7 @@ module TableBuilderHelper ) do link.content end, - class: ('delete-action' if link.method == :delete) + class: (klass.join(' ') if klass.present?) ) end @@ -367,4 +413,10 @@ module TableBuilderHelper # cases, avoid a `NoMethodError`. @__referential__ ||= try(:current_referential) end + + def workgroup + # Certain controllers don't define a `#current_referential`. In these + # cases, avoid a `NoMethodError`. + @__workgroup__ ||= try(:current_workgroup) + end end diff --git a/app/helpers/table_builder_helper/custom_links.rb b/app/helpers/table_builder_helper/custom_links.rb index b1bb11f10..e09078be0 100644 --- a/app/helpers/table_builder_helper/custom_links.rb +++ b/app/helpers/table_builder_helper/custom_links.rb @@ -8,13 +8,14 @@ module TableBuilderHelper unarchive: :put } - attr_reader :actions, :object, :user_context, :referential + attr_reader :actions, :object, :user_context, :referential, :workgroup - def initialize(object, user_context, actions, referential = nil) + def initialize(object, user_context, actions, referential = nil, workgroup = nil) @object = object @user_context = user_context @actions = actions @referential = referential + @workgroup = workgroup end def links @@ -34,7 +35,7 @@ module TableBuilderHelper polymorph_url << action end - polymorph_url += URL.polymorphic_url_parts(object, referential) + polymorph_url += URL.polymorphic_url_parts(object, referential, workgroup) end def method_for_action(action) diff --git a/app/helpers/table_builder_helper/url.rb b/app/helpers/table_builder_helper/url.rb index a53ac5620..0e3dce0aa 100644 --- a/app/helpers/table_builder_helper/url.rb +++ b/app/helpers/table_builder_helper/url.rb @@ -1,6 +1,6 @@ module TableBuilderHelper class URL - def self.polymorphic_url_parts(item, referential) + def self.polymorphic_url_parts(item, referential, workgroup) polymorph_url = [] unless item.is_a?(Calendar) || item.is_a?(Referential) || item.is_a?(ComplianceControlSet) @@ -10,7 +10,7 @@ module TableBuilderHelper polymorph_url << item.route.line if item.is_a?(Chouette::RoutingConstraintZone) polymorph_url << item if item.respond_to? :line_referential polymorph_url << item.stop_area if item.respond_to? :stop_area - polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) + polymorph_url << item if item.respond_to?(:stop_points) || item.is_a?(Chouette::TimeTable) || item.is_a?(Chouette::PurchaseWindow) elsif item.respond_to? :referential if item.respond_to? :workbench polymorph_url << item.workbench @@ -20,6 +20,7 @@ module TableBuilderHelper end end else + polymorph_url << item.workgroup if item.respond_to? :workgroup polymorph_url << item end diff --git a/app/helpers/time_tables_helper.rb b/app/helpers/time_tables_helper.rb index b380a2b0a..1be1fa5a1 100644 --- a/app/helpers/time_tables_helper.rb +++ b/app/helpers/time_tables_helper.rb @@ -84,7 +84,7 @@ module TimeTablesHelper end unless first.wday == first_weekday first.upto(last) do |cur| - cell_text, cell_attrs = block.call(cur) + cell_text, cell_attrs = yield cur cell_text ||= cur.mday cell_attrs ||= {} cell_attrs[:headers] = th_id(cur, options[:table_id]) diff --git a/app/helpers/vehicle_journeys_helper.rb b/app/helpers/vehicle_journeys_helper.rb index 6877abd11..1cc865c62 100644 --- a/app/helpers/vehicle_journeys_helper.rb +++ b/app/helpers/vehicle_journeys_helper.rb @@ -1,5 +1,5 @@ module VehicleJourneysHelper - + def vehicle_name( vehicle) if !vehicle.published_journey_name.blank? vehicle.published_journey_name.first(8) @@ -11,11 +11,11 @@ module VehicleJourneysHelper vehicle.id end end - + def missing_time_check( is_present) return "missing" if (is_present && is_present.departure_time.nil?) end - + def vehicle_departure(vehicle, departure_time=nil) unless departure_time first_vjas = vehicle.vehicle_journey_at_stops.first @@ -24,7 +24,7 @@ module VehicleJourneysHelper end l(departure_time, :format => :hour).gsub( / /, ' ') end - + def vehicle_title(vehicle, journey_frequency=nil) return t("vehicle_journeys.vehicle_journey#{'_frequency' if vehicle.frequency?}.title_stopless", :name => vehicle_name( vehicle)) if vehicle.vehicle_journey_at_stops.empty? first_vjas = vehicle.vehicle_journey_at_stops.first @@ -40,7 +40,7 @@ module VehicleJourneysHelper :time => vehicle_departure(vehicle, (journey_frequency ? journey_frequency.first_departure_time : nil ))) end end - + def route_journey_pattern_label_pairs route route .journey_patterns @@ -50,7 +50,7 @@ module VehicleJourneysHelper def edit_vehicle_title( vehicle) return t('vehicle_journeys.edit.title_stopless', :name => vehicle_name( vehicle)) if vehicle.vehicle_journey_at_stops.empty? first_vjas = vehicle.vehicle_journey_at_stops.first - t('vehicle_journeys.edit.title', + t('vehicle_journeys.edit.title', :name => vehicle_name( vehicle), :stop => first_vjas.stop_point.stop_area.name, :time => vehicle_departure(vehicle)) @@ -59,6 +59,14 @@ module VehicleJourneysHelper def exist_vehicle_journeys?(route) route.vehicle_journeys.count > 0 end - -end + def table_builder_column_for_stop_area stop_area + return nil unless stop_area + TableBuilderHelper::Column.new( + name: stop_area.name, + attribute: Proc.new {|v| v.vehicle_journey_at_stops.find{|vjas| vjas.stop_point.stop_area_id == stop_area.id}&.departure }, + sortable: false + ) + end + +end diff --git a/app/inputs/color_select_input.rb b/app/inputs/color_select_input.rb new file mode 100644 index 000000000..f92c80a22 --- /dev/null +++ b/app/inputs/color_select_input.rb @@ -0,0 +1,44 @@ +class ColorSelectInput < SimpleForm::Inputs::CollectionInput + enable :placeholder + + def input(wrapper_options = {}) + selected_color = object.send(attribute_name) + label = if selected_color + collection.find{|i| i.is_a?(Enumerable) && i.last == selected_color}.try(:first) + end + + out = @builder.hidden_field attribute_name, value: selected_color + tag_name = ActionView::Helpers::Tags::Base.new( ActiveModel::Naming.param_key(object), attribute_name, :dummy ).send(:tag_name) + select = <<-eos + <div class="dropdown color_selector"> + <button type='button' class="btn btn-default dropdown-toggle" data-toggle='dropdown' aria-haspopup='true' aria-expanded='true' + ><span + class='fa fa-circle mr-xs' + style='color: #{selected_color == nil ? 'transparent' : selected_color}' + > + </span> + #{label} + <span class='caret'></span> + </button> + + <div class="form-group dropdown-menu" aria-labelledby='dpdwn_color'> + eos + + collection.each do |color| + name = nil + name, color = color if color.is_a?(Enumerable) + select += <<-eos + <span class="radio" key=#{color} > + <label> + <input type='radio' class='color_selector' value='#{color}' data-for='#{tag_name}'/> + <span class='fa fa-circle mr-xs' style='color: #{color == nil ? 'transparent' : color}'></span> + #{name} + </label> + </span> + eos + end + select += "</div></div>" + + out + select.html_safe + end +end diff --git a/app/javascript/date_filters/index.js b/app/javascript/date_filters/index.js index ee892a7fe..432166008 100644 --- a/app/javascript/date_filters/index.js +++ b/app/javascript/date_filters/index.js @@ -3,6 +3,7 @@ import complianceControlSetDF from './compliance_control_set' import complianceCheckSetDF from './compliance_check_set' import timetableDF from './time_table' import importDF from './import' +import purchaseWindowDF from './purchase_window' import workbenchDF from './workbench' const DateFilters = { @@ -11,6 +12,7 @@ const DateFilters = { complianceControlSetDF, importDF, timetableDF, + purchaseWindowDF, workbenchDF } diff --git a/app/javascript/date_filters/purchase_window.js b/app/javascript/date_filters/purchase_window.js new file mode 100644 index 000000000..2c46b6d52 --- /dev/null +++ b/app/javascript/date_filters/purchase_window.js @@ -0,0 +1,5 @@ +import DateFilter from '../helpers/date_filters' + +const purchaseWindowDF = new DateFilter("purchase_window_filter_btn", "Tous les champs du filtre de date doivent être remplis", "q_contains_date_NUMi") + +export default purchaseWindowDF
\ No newline at end of file diff --git a/app/javascript/helpers/master_slave.coffee b/app/javascript/helpers/master_slave.coffee new file mode 100644 index 000000000..81bebe36a --- /dev/null +++ b/app/javascript/helpers/master_slave.coffee @@ -0,0 +1,16 @@ +class MasterSlave + constructor: (selector)-> + $(selector).find('[data-master]').each (i, slave)-> + $slave = $(slave) + master = $($slave.data().master) + $slave.find("input:disabled, select:disabled").attr "data-slave-force-disabled", "true" + toggle = -> + val = master.filter(":checked").val() if master.filter("[type=radio]").length > 0 + val ||= master.val() + selected = val == $slave.data().value + $slave.toggle selected + $slave.find("input, select").filter(":not([data-slave-force-disabled])").attr "disabled", !selected + master.change toggle + toggle() + +export default MasterSlave diff --git a/app/javascript/helpers/routes_map.coffee b/app/javascript/helpers/routes_map.coffee new file mode 100644 index 000000000..6834406fc --- /dev/null +++ b/app/javascript/helpers/routes_map.coffee @@ -0,0 +1,161 @@ +class RoutesMap + constructor: (@target)-> + @initMap() + @area = [] + @seenStopIds = [] + @routes = {} + + initMap: -> + @map = new ol.Map + target: @target, + layers: [ new ol.layer.Tile(source: new ol.source.OSM()) ] + controls: [ new ol.control.ScaleLine(), new ol.control.Zoom(), new ol.control.ZoomSlider() ], + interactions: ol.interaction.defaults(zoom: true) + view: new ol.View() + + addRoutes: (routes)-> + for route in routes + @addRoute route + + addRoute: (route)-> + geoColPts = [] + geoColLns = [] + @routes[route.id] = route if route.id + stops = route.stops || route + geoColEdges = [ + new ol.Feature({ + geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stops[0].longitude), parseFloat(stops[0].latitude)])) + }), + new ol.Feature({ + geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stops[stops.length - 1].longitude), parseFloat(stops[stops.length - 1].latitude)])) + }) + ] + + prevStop = null + stops.forEach (stop, i) => + if stop.longitude && stop.latitude + if prevStop + geoColLns.push new ol.Feature + geometry: new ol.geom.LineString([ + ol.proj.fromLonLat([parseFloat(prevStop.longitude), parseFloat(prevStop.latitude)]), + ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)]) + ]) + prevStop = stop + + geoColPts.push(new ol.Feature({ + geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)])) + })) + unless @seenStopIds.indexOf(stop.stoparea_id) > 0 + @area.push [parseFloat(stop.longitude), parseFloat(stop.latitude)] + @seenStopIds.push stop.stoparea_id + + vectorPtsLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: geoColPts + }), + style: @defaultStyles(), + zIndex: 2 + }) + route.vectorPtsLayer = vectorPtsLayer if route.id + vectorEdgesLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: geoColEdges + }), + style: @edgeStyles(), + zIndex: 3 + }) + route.vectorEdgesLayer = vectorEdgesLayer if route.id + vectorLnsLayer = new ol.layer.Vector({ + source: new ol.source.Vector({ + features: geoColLns + }), + style: [@lineStyle()], + zIndex: 1 + }) + route.vectorLnsLayer = vectorLnsLayer if route.id + @map.addLayer vectorPtsLayer + @map.addLayer vectorEdgesLayer + @map.addLayer vectorLnsLayer + + lineStyle: (highlighted=false)-> + new ol.style.Style + stroke: new ol.style.Stroke + color: if highlighted then "#ed7f00" else '#007fbb' + width: 3 + + edgeStyles: (highlighted=false)-> + new ol.style.Style + image: new ol.style.Circle + radius: 5 + stroke: new ol.style.Stroke + color: if highlighted then "#ed7f00" else '#007fbb' + width: 2 + fill: new ol.style.Fill + color: if highlighted then "#ed7f00" else '#007fbb' + width: 2 + + defaultStyles: (highlighted=false)-> + new ol.style.Style + image: new ol.style.Circle + radius: 4 + stroke: new ol.style.Stroke + color: if highlighted then "#ed7f00" else '#007fbb' + width: 2 + fill: new ol.style.Fill + color: '#ffffff' + width: 2 + + addRoutesLabels: -> + labelsContainer = $("<ul class='routes-labels'></ul>") + labelsContainer.appendTo $("##{@target}") + @vectorPtsLayer = null + @vectorEdgesLayer = null + @vectorLnsLayer = null + Object.keys(@routes).forEach (id)=> + route = @routes[id] + label = $("<li>#{route.name}</ul>") + label.appendTo labelsContainer + label.mouseleave => + route.vectorPtsLayer.setStyle @defaultStyles(false) + route.vectorEdgesLayer.setStyle @edgeStyles(false) + route.vectorLnsLayer.setStyle @lineStyle(false) + route.vectorPtsLayer.setZIndex 2 + route.vectorEdgesLayer.setZIndex 3 + route.vectorLnsLayer.setZIndex 1 + @fitZoom() + label.mouseenter => + route.vectorPtsLayer.setStyle @defaultStyles(true) + route.vectorEdgesLayer.setStyle @edgeStyles(true) + route.vectorLnsLayer.setStyle @lineStyle(true) + route.vectorPtsLayer.setZIndex 11 + route.vectorEdgesLayer.setZIndex 12 + route.vectorLnsLayer.setZIndex 10 + @fitZoom(route) + + fitZoom: (route)-> + if route + area = [] + route.stops.forEach (stop, i) => + area.push [parseFloat(stop.longitude), parseFloat(stop.latitude)] + else + area = @area + boundaries = ol.extent.applyTransform( + ol.extent.boundingExtent(area), ol.proj.getTransform('EPSG:4326', 'EPSG:3857') + ) + @map.getView().fit boundaries, @map.getSize() + tooCloseToBounds = false + mapBoundaries = @map.getView().calculateExtent @map.getSize() + mapWidth = mapBoundaries[2] - mapBoundaries[0] + mapHeight = mapBoundaries[3] - mapBoundaries[1] + marginSize = 0.1 + heightMargin = marginSize * mapHeight + widthMargin = marginSize * mapWidth + tooCloseToBounds = tooCloseToBounds || (boundaries[0] - mapBoundaries[0]) < widthMargin + tooCloseToBounds = tooCloseToBounds || (mapBoundaries[2] - boundaries[2]) < widthMargin + tooCloseToBounds = tooCloseToBounds || (boundaries[1] - mapBoundaries[1]) < heightMargin + tooCloseToBounds = tooCloseToBounds || (mapBoundaries[3] - boundaries[3]) < heightMargin + if tooCloseToBounds + @map.getView().setZoom(@map.getView().getZoom() - 1) + + +export default RoutesMap diff --git a/app/javascript/helpers/save_button.js b/app/javascript/helpers/save_button.js new file mode 100644 index 000000000..7e0bd5bbe --- /dev/null +++ b/app/javascript/helpers/save_button.js @@ -0,0 +1,47 @@ +import React, { PropTypes, Component } from 'react' + +export default class SaveButton extends Component{ + constructor(props){ + super(props) + } + + btnDisabled(){ + return !this.props.status.fetchSuccess || this.props.status.isFetching + } + + btnClass(){ + let className = ['btn btn-default'] + if(this.btnDisabled()){ + className.push('disabled') + } + return className.join(' ') + } + + render() { + if (!this.hasPolicy()) { + return false + }else{ + return ( + <div className='row mt-md'> + <div className='col-lg-12 text-right'> + <form className={this.formClassName() + ' formSubmitr ml-xs'} onSubmit={e => {e.preventDefault()}}> + <div className="btn-group sticky-actions"> + <button + className={this.btnClass()} + type='button' + disabled={this.btnDisabled()} + onClick={e => { + e.preventDefault() + this.props.editMode ? this.submitForm() : this.props.onEnterEditMode() + }} + > + {this.props.editMode ? "Valider" : "Editer"} + </button> + </div> + </form> + </div> + </div> + ) + } + } +} diff --git a/app/javascript/helpers/stop_area_header_manager.js b/app/javascript/helpers/stop_area_header_manager.js new file mode 100644 index 000000000..5b18e2f63 --- /dev/null +++ b/app/javascript/helpers/stop_area_header_manager.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react' + +export default class StopAreaHeaderManager { + constructor(ids_list, stopPointsList, features) { + this.ids_list = ids_list + this.stopPointsList = stopPointsList + this.features = features + } + + hasFeature(key) { + return this.features[key] + } + + stopPointHeader(object_id) { + let index = this.ids_list.indexOf(object_id) + let sp = this.stopPointsList[index] + let showHeadline = this.showHeader(object_id) + return ( + <div + className={(showHeadline) ? 'headlined' : ''} + data-headline={showHeadline} + title={sp.city_name ? sp.city_name + ' (' + sp.zip_code +')' : ""} + > + <span> + <span> + {sp.name} + {sp.time_zone_formatted_offset && <span className="small"> + ({sp.time_zone_formatted_offset}) + </span>} + {sp.area_kind == 'non_commercial' && <span className="fa fa-question-circle" title={sp.area_type_i18n}> + </span>} + </span> + </span> + </div> + ) + } + + showHeader(object_id) { + let showHeadline = false + let headline = "" + let attribute_to_check = this.hasFeature('long_distance_routes') ? "country_code" : "city_name" + let index = this.ids_list.indexOf(object_id) + let sp = this.stopPointsList[index] + let previousBreakpoint = this.stopPointsList[index - 1] + if(sp == undefined){ + console.log("STOP_POINT NOT FOUND: " + object_id) + console.log("AVAILABLE IDS:" + this.ids_list) + return + } + if(index == 0 || (sp[attribute_to_check] != previousBreakpoint[attribute_to_check])){ + showHeadline = true + headline = this.hasFeature('long_distance_routes') ? sp.country_name : sp.city_name + } + return showHeadline ? headline : "" + } +} diff --git a/app/javascript/journey_patterns/actions/index.js b/app/javascript/journey_patterns/actions/index.js index 4ff3f77ea..a70a2e6f2 100644 --- a/app/javascript/journey_patterns/actions/index.js +++ b/app/javascript/journey_patterns/actions/index.js @@ -64,6 +64,11 @@ const actions = { type : 'DELETE_JOURNEYPATTERN', index, }), + updateJourneyPatternCosts : (index, costs) => ({ + type : 'UPDATE_JOURNEYPATTERN_COSTS', + index, + costs + }), closeModal : () => ({ type : 'CLOSE_MODAL' }), @@ -194,15 +199,13 @@ const actions = { } }) } - journeyPatterns.push({ - name: val.name, - object_id: val.object_id, - short_id: val.short_id, - published_name: val.published_name, - registration_number: val.registration_number, - stop_points: val.route_short_description.stop_points, - deletable: false - }) + journeyPatterns.push( + _.assign({}, val, { + stop_points: val.route_short_description.stop_points, + costs: val.costs || {}, + deletable: false + }) + ) } } window.currentItemsLength = journeyPatterns.length @@ -217,4 +220,4 @@ const actions = { } } -export default actions
\ No newline at end of file +export default actions diff --git a/app/javascript/journey_patterns/components/ConfirmModal.js b/app/javascript/journey_patterns/components/ConfirmModal.js index 2cc1bef44..ccd0a9384 100644 --- a/app/javascript/journey_patterns/components/ConfirmModal.js +++ b/app/javascript/journey_patterns/components/ConfirmModal.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, journeyPatterns}) { return ( diff --git a/app/javascript/journey_patterns/components/CreateModal.js b/app/javascript/journey_patterns/components/CreateModal.js index d0eff6e57..a6c1b608a 100644 --- a/app/javascript/journey_patterns/components/CreateModal.js +++ b/app/javascript/journey_patterns/components/CreateModal.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class CreateModal extends Component { diff --git a/app/javascript/journey_patterns/components/EditModal.js b/app/javascript/journey_patterns/components/EditModal.js index e7ce24aa1..c960cb41c 100644 --- a/app/javascript/journey_patterns/components/EditModal.js +++ b/app/javascript/journey_patterns/components/EditModal.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class EditModal extends Component { @@ -36,7 +37,6 @@ export default class EditModal extends Component { {this.renderModalTitle()} <span type="button" className="close modal-close" data-dismiss="modal">×</span> </div> - {(this.props.modal.type == 'edit') && ( <form> <div className='modal-body'> @@ -85,6 +85,16 @@ export default class EditModal extends Component { </div> </div> </div> + <div> + <label className='control-label'>Signature métier</label> + <input + type='text' + ref='checksum' + className='form-control' + disabled='disabled' + defaultValue={this.props.modal.modalProps.journeyPattern.checksum} + /> + </div> </div> { this.props.editMode && @@ -121,4 +131,4 @@ EditModal.propTypes = { modal: PropTypes.object, onModalClose: PropTypes.func.isRequired, saveModal: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/journey_patterns/components/JourneyPattern.js b/app/javascript/journey_patterns/components/JourneyPattern.js index d4c9816ec..35765b99a 100644 --- a/app/javascript/journey_patterns/components/JourneyPattern.js +++ b/app/javascript/journey_patterns/components/JourneyPattern.js @@ -1,10 +1,21 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class JourneyPattern extends Component{ constructor(props){ super(props) - this.previousCity = undefined + this.previousSpId = undefined + this.updateCosts = this.updateCosts.bind(this) + } + + updateCosts(e) { + let costs = { + [e.target.dataset.costsKey]: { + [e.target.name]: parseFloat(e.target.value) + } + } + this.props.onUpdateJourneyPatternCosts(costs) } vehicleJourneyURL(jpOid) { @@ -16,16 +27,20 @@ export default class JourneyPattern extends Component{ ) } - cityNameChecker(sp) { - let bool = false - if(sp.city_name != this.previousCity){ - bool = true - this.previousCity = sp.city_name - } + hasFeature(key) { + return this.props.status.features[key] + } + + cityNameChecker(sp, i) { + return this.props.journeyPatterns.showHeader((sp.stop_area_object_id || sp.object_id) + "-" + i) + } + + spNode(sp, headlined){ return ( <div - className={(bool) ? 'headlined' : ''} + className={(headlined) ? 'headlined' : ''} > + <div className={'link '}></div> <span className='has_radio'> <input onChange = {(e) => this.props.onCheckboxChange(e)} @@ -59,19 +74,60 @@ export default class JourneyPattern extends Component{ return !this.props.status.policy[`journey_patterns.${action}`] } - render() { - this.previousCity = undefined + totals(){ + let totalTime = 0 + let totalDistance = 0 + let from = null + this.props.value.stop_points.map((stopPoint, i) =>{ + if(from && stopPoint.checked){ + let [costsKey, costs, time, distance] = this.getTimeAndDistanceBetweenStops(from, stopPoint.id) + totalTime += time + totalDistance += distance + } + if(stopPoint.checked){ + from = stopPoint.id + } + }) + return [this.formatTime(totalTime), this.formatDistance(totalDistance)] + } - return ( - <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.object_id ? '' : ' to_record') + (this.props.value.errors ? ' has-error': '')}> - {/* Errors */} - {/* this.props.value.errors ? this.getErrors(this.props.value.errors) : '' */} + getTimeAndDistanceBetweenStops(from, to){ + let costsKey = from + "-" + to + let costs = this.props.value.costs[costsKey] || {distance: 0, time: 0} + let time = costs['time'] || 0 + let distance = costs['distance'] || 0 + return [costsKey, costs, time, distance] + } + + formatDistance(distance){ + return parseFloat(Math.round(distance * 100) / 100).toFixed(2) + " km" + } + + formatTime(time){ + if(time < 60){ + return time + " min" + } + else{ + let hours = parseInt(time/60) + return hours + " h " + (time - 60*hours) + } + } + render() { + this.previousSpId = undefined + let [totalTime, totalDistance] = this.totals() + return ( + <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.object_id ? '' : ' to_record') + (this.props.value.errors ? ' has-error': '') + (this.hasFeature('costs_in_journey_patterns') ? ' with-costs' : '')}> <div className='th'> <div className='strong mb-xs'>{this.props.value.object_id ? this.props.value.short_id : '-'}</div> <div>{this.props.value.registration_number}</div> <div>{actions.getChecked(this.props.value.stop_points).length} arrêt(s)</div> - + {this.hasFeature('costs_in_journey_patterns') && + <div className="small row totals"> + <span className="col-md-6"><i className="fa fa-arrows-h"></i>{totalDistance}</span> + <span className="col-md-6"><i className="fa fa-clock-o"></i>{totalTime}</span> + </div> + } <div className={this.props.value.deletable ? 'btn-group disabled' : 'btn-group'}> <div className={this.props.value.deletable ? 'btn dropdown-toggle disabled' : 'btn dropdown-toggle'} @@ -80,10 +136,9 @@ export default class JourneyPattern extends Component{ <span className='fa fa-cog'></span> </div> <ul className='dropdown-menu'> - <li className={this.isDisabled('update') ? 'disabled' : ''}> + <li> <button type='button' - disabled={this.isDisabled('update')} onClick={this.props.onOpenEditModal} data-toggle='modal' data-target='#JourneyPatternModal' @@ -112,9 +167,40 @@ export default class JourneyPattern extends Component{ </div> {this.props.value.stop_points.map((stopPoint, i) =>{ + let costs = null + let costsKey = null + let time = null + let distance = null + let time_in_words = null + if(this.previousSpId && stopPoint.checked){ + [costsKey, costs, time, distance] = this.getTimeAndDistanceBetweenStops(this.previousSpId, stopPoint.id) + time_in_words = this.formatTime(time) + } + if(stopPoint.checked){ + this.previousSpId = stopPoint.id + } + let headlined = this.cityNameChecker(stopPoint, i) return ( - <div key={i} className='td'> - {this.cityNameChecker(stopPoint)} + <div key={i} className={(stopPoint.checked ? 'activated' : 'deactivated') + (this.props.editMode ? ' edit-mode' : '')}> + <div className={'td' + (headlined ? ' with-headline' : '')}> + {this.spNode(stopPoint, headlined)} + </div> + {this.hasFeature('costs_in_journey_patterns') && costs && <div className='costs' id={'costs-' + this.props.value.id + '-' + costsKey }> + {this.props.editMode && <div> + <p> + <input type="number" value={costs['distance'] || 0} min='0' name="distance" step="0.01" onChange={this.updateCosts} data-costs-key={costsKey}/> + <span>km</span> + </p> + <p> + <input type="number" value={costs['time'] || 0} min='0' name="time" onChange={this.updateCosts} data-costs-key={costsKey}/> + <span>min</span> + </p> + </div>} + {!this.props.editMode && <div> + <p><i className="fa fa-arrows-h"></i>{this.formatDistance(costs['distance'] || 0)}</p> + <p><i className="fa fa-clock-o"></i>{time_in_words}</p> + </div>} + </div>} </div> ) })} @@ -128,5 +214,6 @@ JourneyPattern.propTypes = { index: PropTypes.number, onCheckboxChange: PropTypes.func.isRequired, onOpenEditModal: PropTypes.func.isRequired, - onDeleteJourneyPattern: PropTypes.func.isRequired -}
\ No newline at end of file + onDeleteJourneyPattern: PropTypes.func.isRequired, + journeyPatterns: PropTypes.object.isRequired +} diff --git a/app/javascript/journey_patterns/components/JourneyPatterns.js b/app/javascript/journey_patterns/components/JourneyPatterns.js index 4b2badabb..31727fefc 100644 --- a/app/javascript/journey_patterns/components/JourneyPatterns.js +++ b/app/javascript/journey_patterns/components/JourneyPatterns.js @@ -1,16 +1,23 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import _ from 'lodash' import JourneyPattern from './JourneyPattern' - +import StopAreaHeaderManager from '../../helpers/stop_area_header_manager' export default class JourneyPatterns extends Component { constructor(props){ super(props) - this.previousCity = undefined + this.headerManager = new StopAreaHeaderManager( + _.map(this.props.stopPointsList, (sp, i)=>{return sp.stop_area_object_id + "-" + i}), + this.props.stopPointsList, + this.props.status.features + ) } + componentDidMount() { this.props.onLoadFirstPage() } + componentDidUpdate(prevProps, prevState) { if(this.props.status.isFetching == false){ $('.table-2entries').each(function() { @@ -54,21 +61,12 @@ export default class JourneyPatterns extends Component { } } - cityNameChecker(sp) { - let bool = false - if(sp.city_name != this.previousCity){ - bool = true - this.previousCity = sp.city_name - } - return ( - <div - className={(bool) ? 'headlined' : ''} - data-headline={(bool) ? sp.city_name : ''} - title={sp.city_name + ' (' + sp.zip_code +')'} - > - <span><span>{sp.name}</span></span> - </div> - ) + showHeader(object_id) { + return this.headerManager.showHeader(object_id) + } + + hasFeature(key) { + return this.props.status.features[key] } render() { @@ -115,8 +113,8 @@ export default class JourneyPatterns extends Component { </div> {this.props.stopPointsList.map((sp, i) =>{ return ( - <div key={i} className='td'> - {this.cityNameChecker(sp)} + <div key={i} className={'td' + (this.hasFeature('costs_in_journey_patterns') ? ' with-costs' : '')}> + {this.headerManager.stopPointHeader(sp.stop_area_object_id + "-" + i)} </div> ) })} @@ -131,8 +129,10 @@ export default class JourneyPatterns extends Component { onCheckboxChange= {(e) => this.props.onCheckboxChange(e, index)} onOpenEditModal= {() => this.props.onOpenEditModal(index, journeyPattern)} onDeleteJourneyPattern={() => this.props.onDeleteJourneyPattern(index)} + onUpdateJourneyPatternCosts={(costs) => this.props.onUpdateJourneyPatternCosts(index, costs)} status= {this.props.status} editMode= {this.props.editMode} + journeyPatterns= {this} /> )} </div> @@ -152,4 +152,4 @@ JourneyPatterns.propTypes = { onCheckboxChange: PropTypes.func.isRequired, onLoadFirstPage: PropTypes.func.isRequired, onOpenEditModal: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/journey_patterns/components/Navigate.js b/app/javascript/journey_patterns/components/Navigate.js index f2fdd668f..78f324a7d 100644 --- a/app/javascript/journey_patterns/components/Navigate.js +++ b/app/javascript/journey_patterns/components/Navigate.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default function Navigate({ dispatch, journeyPatterns, pagination, status }) { diff --git a/app/javascript/journey_patterns/components/SaveJourneyPattern.js b/app/javascript/journey_patterns/components/SaveJourneyPattern.js index d071fa542..4bb6a73a0 100644 --- a/app/javascript/journey_patterns/components/SaveJourneyPattern.js +++ b/app/javascript/journey_patterns/components/SaveJourneyPattern.js @@ -1,34 +1,19 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SaveButton from '../../helpers/save_button' import actions from '../actions' -export default class SaveJourneyPattern extends Component { - constructor(props){ - super(props) +export default class SaveJourneyPattern extends SaveButton { + hasPolicy(){ + return this.props.status.policy['journey_patterns.update'] == true } - render() { - if(this.props.status.policy['journey_patterns.update'] == false) { - return false - }else{ - return ( - <div className='row mt-md'> - <div className='col-lg-12 text-right'> - <form className='jp_collection formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}> - <button - className='btn btn-default' - type='button' - onClick={e => { - e.preventDefault() - this.props.editMode ? this.props.onSubmitJourneyPattern(this.props.dispatch, this.props.journeyPatterns) : this.props.onEnterEditMode() - }} - > - {this.props.editMode ? "Valider" : "Editer"} - </button> - </form> - </div> - </div> - ) - } + formClassName(){ + return 'jp_collection' + } + + submitForm(){ + this.props.onSubmitJourneyPattern(this.props.dispatch, this.props.journeyPatterns) } } @@ -36,4 +21,4 @@ SaveJourneyPattern.propTypes = { journeyPatterns: PropTypes.array.isRequired, status: PropTypes.object.isRequired, page: PropTypes.number.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/journey_patterns/containers/JourneyPatternList.js b/app/javascript/journey_patterns/containers/JourneyPatternList.js index d98734407..d338345f2 100644 --- a/app/javascript/journey_patterns/containers/JourneyPatternList.js +++ b/app/javascript/journey_patterns/containers/JourneyPatternList.js @@ -25,7 +25,10 @@ const mapDispatchToProps = (dispatch) => { }, onDeleteJourneyPattern: (index) =>{ dispatch(actions.deleteJourneyPattern(index)) - } + }, + onUpdateJourneyPatternCosts: (index, costs) =>{ + dispatch(actions.updateJourneyPatternCosts(index, costs)) + }, } } diff --git a/app/javascript/journey_patterns/reducers/journeyPatterns.js b/app/javascript/journey_patterns/reducers/journeyPatterns.js index 0bbcba976..1ce069522 100644 --- a/app/javascript/journey_patterns/reducers/journeyPatterns.js +++ b/app/javascript/journey_patterns/reducers/journeyPatterns.js @@ -17,6 +17,7 @@ const journeyPattern = (state = {}, action) =>{ published_name: action.data.published_name.value, registration_number: action.data.registration_number.value, stop_points: stopPoints, + costs: {}, deletable: false } case 'UPDATE_CHECKBOX_VALUE': @@ -67,6 +68,19 @@ export default function journeyPatterns (state = [], action) { return j } }) + case 'UPDATE_JOURNEYPATTERN_COSTS': + return state.map((j, i) =>{ + if(i == action.index) { + const new_costs = Object.assign({}, j.costs) + Object.keys(action.costs).map((key) => { + let new_costs_for_key = Object.assign({}, j.costs[key] || {}, action.costs[key]) + new_costs[key] = new_costs_for_key + }) + return _.assign({}, j, {costs: new_costs}) + } else { + return j + } + }) case 'ADD_JOURNEYPATTERN': return [ journeyPattern(state, action), @@ -87,4 +101,4 @@ export default function journeyPatterns (state = [], action) { default: return state } -}
\ No newline at end of file +} diff --git a/app/javascript/packs/calendars/edit.js b/app/javascript/packs/calendars/edit.js new file mode 100644 index 000000000..bd09657ec --- /dev/null +++ b/app/javascript/packs/calendars/edit.js @@ -0,0 +1,74 @@ +import React from 'react' +import { render } from 'react-dom' +import { Provider } from 'react-redux' +import { createStore } from 'redux' +import timeTablesApp from '../../time_tables/reducers' +import App from '../../time_tables/containers/App' +import clone from '../../helpers/clone' + +const actionType = clone(window, "actionType", true) + +// logger, DO NOT REMOVE +// var applyMiddleware = require('redux').applyMiddleware +// var createLogger = require('redux-logger') +// var thunkMiddleware = require('redux-thunk').default +// var promise = require('redux-promise') + +let initialState = { + status: { + actionType: actionType, + policy: window.perms, + fetchSuccess: true, + isFetching: false + }, + timetable: { + current_month: [], + current_periode_range: '', + periode_range: [], + time_table_periods: [], + time_table_dates: [] + }, + metas: { + comment: '', + day_types: [], + initial_tags: [] + }, + pagination: { + stateChanged: false, + currentPage: '', + periode_range: [] + }, + modal: { + type: '', + modalProps: { + active: false, + begin: { + day: '01', + month: '01', + year: String(new Date().getFullYear()) + }, + end: { + day: '01', + month: '01', + year: String(new Date().getFullYear()) + }, + index: false, + error: '' + }, + confirmModal: {} + } +} +// const loggerMiddleware = createLogger() + +let store = createStore( + timeTablesApp, + initialState, + // applyMiddleware(thunkMiddleware, promise, loggerMiddleware) +) + +render( + <Provider store={store}> + <App /> + </Provider>, + document.getElementById('periods') +) diff --git a/app/javascript/packs/journey_patterns/index.js b/app/javascript/packs/journey_patterns/index.js index fde28b45d..367a8830f 100644 --- a/app/javascript/packs/journey_patterns/index.js +++ b/app/javascript/packs/journey_patterns/index.js @@ -16,6 +16,7 @@ var initialState = { editMode: false, status: { policy: window.perms, + features: window.features, fetchSuccess: true, isFetching: false }, diff --git a/app/javascript/packs/referential_lines/show.js b/app/javascript/packs/referential_lines/show.js new file mode 100644 index 000000000..542188018 --- /dev/null +++ b/app/javascript/packs/referential_lines/show.js @@ -0,0 +1,10 @@ +import clone from '../../helpers/clone' +import RoutesMap from '../../helpers/routes_map' + +let routes = clone(window, "routes", true) +routes = JSON.parse(decodeURIComponent(routes)) + +var map = new RoutesMap('routes_map') +map.addRoutes(routes) +// map.addRoutesLabels() +map.fitZoom() diff --git a/app/javascript/packs/referential_overview/overview.js b/app/javascript/packs/referential_overview/overview.js new file mode 100644 index 000000000..59c326e9a --- /dev/null +++ b/app/javascript/packs/referential_overview/overview.js @@ -0,0 +1 @@ +import ReferentialOverview from '../../referential_overview' diff --git a/app/javascript/packs/routes/edit.js b/app/javascript/packs/routes/edit.js index d6ceed60f..b787bec97 100644 --- a/app/javascript/packs/routes/edit.js +++ b/app/javascript/packs/routes/edit.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' @@ -12,7 +14,7 @@ datas = JSON.parse(decodeURIComponent(datas)) // logger, DO NOT REMOVE var applyMiddleware = require('redux').applyMiddleware -var createLogger = require('redux-logger') +import {createLogger} from 'redux-logger'; var thunkMiddleware = require('redux-thunk').default var promise = require('redux-promise') diff --git a/app/javascript/packs/routes/show.js b/app/javascript/packs/routes/show.js index 7f14a6f11..c20de0800 100644 --- a/app/javascript/packs/routes/show.js +++ b/app/javascript/packs/routes/show.js @@ -1,102 +1,8 @@ import clone from '../../helpers/clone' +import RoutesMap from '../../helpers/routes_map' + let route = clone(window, "route", true) route = JSON.parse(decodeURIComponent(route)) - -const geoColPts = [] -const geoColLns = [] -const geoColEdges = [ - new ol.Feature({ - geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)])) - }), - new ol.Feature({ - geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[route.length - 1].longitude), parseFloat(route[route.length - 1].latitude)])) - }) -] -route.forEach(function (stop, i) { - if (i < route.length - 1) { - geoColLns.push(new ol.Feature({ - geometry: new ol.geom.LineString([ - ol.proj.fromLonLat([parseFloat(route[i].longitude), parseFloat(route[i].latitude)]), - ol.proj.fromLonLat([parseFloat(route[i + 1].longitude), parseFloat(route[i + 1].latitude)]) - ]) - })) - } - geoColPts.push(new ol.Feature({ - geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)])) - }) - ) -}) -var edgeStyles = new ol.style.Style({ - image: new ol.style.Circle(({ - radius: 5, - stroke: new ol.style.Stroke({ - color: '#007fbb', - width: 2 - }), - fill: new ol.style.Fill({ - color: '#007fbb', - width: 2 - }) - })) -}) -var defaultStyles = new ol.style.Style({ - image: new ol.style.Circle(({ - radius: 4, - stroke: new ol.style.Stroke({ - color: '#007fbb', - width: 2 - }), - fill: new ol.style.Fill({ - color: '#ffffff', - width: 2 - }) - })) -}) -var lineStyle = new ol.style.Style({ - stroke: new ol.style.Stroke({ - color: '#007fbb', - width: 3 - }) -}) - -var vectorPtsLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - features: geoColPts - }), - style: defaultStyles, - zIndex: 2 -}) -var vectorEdgesLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - features: geoColEdges - }), - style: edgeStyles, - zIndex: 3 -}) -var vectorLnsLayer = new ol.layer.Vector({ - source: new ol.source.Vector({ - features: geoColLns - }), - style: [lineStyle], - zIndex: 1 -}) - -var map = new ol.Map({ - target: 'route_map', - layers: [ - new ol.layer.Tile({ - source: new ol.source.OSM() - }), - vectorPtsLayer, - vectorEdgesLayer, - vectorLnsLayer - ], - controls: [new ol.control.ScaleLine(), new ol.control.Zoom(), new ol.control.ZoomSlider()], - interactions: ol.interaction.defaults({ - zoom: true - }), - view: new ol.View({ - center: ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]), - zoom: 13 - }) -}); +var map = new RoutesMap('route_map') +map.addRoute(route) +map.fitZoom() diff --git a/app/javascript/packs/stop_areas/new.js b/app/javascript/packs/stop_areas/new.js new file mode 100644 index 000000000..ffe702cdb --- /dev/null +++ b/app/javascript/packs/stop_areas/new.js @@ -0,0 +1,3 @@ +import MasterSlave from "../../helpers/master_slave" + +new MasterSlave("form") diff --git a/app/javascript/packs/vehicle_journeys/index.js b/app/javascript/packs/vehicle_journeys/index.js index 38431af1d..e6867cb17 100644 --- a/app/javascript/packs/vehicle_journeys/index.js +++ b/app/javascript/packs/vehicle_journeys/index.js @@ -23,6 +23,7 @@ var initialState = { filters: { selectedJourneyPatterns : selectedJP, policy: window.perms, + features: window.features, toggleArrivals: false, queryString: '', query: { @@ -54,11 +55,12 @@ var initialState = { }, status: { - fetchSuccess: true, + fetchSuccess: false, isFetching: false }, vehicleJourneys: [], stopPointsList: window.stopPoints, + returnStopPointsList: window.returnStopPoints, pagination: { page : 1, totalCount: 0, @@ -69,7 +71,9 @@ var initialState = { type: '', modalProps: {}, confirmModal: {} - } + }, + missions: window.all_missions, + custom_fields: window.custom_fields } if (window.jpOrigin){ @@ -96,7 +100,7 @@ let store = createStore( render( <Provider store={store}> - <App /> + <App returnRouteUrl={window.returnRouteUrl} /> </Provider>, document.getElementById('vehicle_journeys_wip') -)
\ No newline at end of file +) diff --git a/app/javascript/referential_overview/index.coffee b/app/javascript/referential_overview/index.coffee new file mode 100644 index 000000000..0e6541421 --- /dev/null +++ b/app/javascript/referential_overview/index.coffee @@ -0,0 +1,113 @@ +class TimeTravel + constructor: (@overview)-> + @container = @overview.container.find('.time-travel') + @todayBt = @container.find(".today") + @prevBt = @container.find(".prev-page") + @nextBt = @container.find(".next-page") + @searchDateBt = @container.find("a.search-date") + @searchDateInput = @container.find("input.date-search") + @initButtons() + + initButtons: -> + @prevBt.click (e)=> + @overview.prevPage() + e.preventDefault() + false + + @nextBt.click (e)=> + @overview.nextPage() + e.preventDefault() + false + + @todayBt.click (e)=> + today = new Date() + month = today.getMonth() + 1 + month = "0#{month}" if month < 10 + day = today.getDate() + day = "0#{month}" if day < 10 + @overview.showDay "#{today.getFullYear()}-#{month}-#{day}" + e.preventDefault() + false + + @searchDateBt.click (e)=> + @overview.showDay @searchDateInput.val() if @searchDateInput.val().length > 0 + e.preventDefault() + false + + scrolledTo: (progress)-> + @prevBt.removeClass 'disabled' + @nextBt.removeClass 'disabled' + @prevBt.addClass 'disabled' if progress == 0 + @nextBt.addClass 'disabled' if progress == 1 + +class window.ReferentialOverview + constructor: (selector)-> + @container = $(selector) + @timeTravel = new TimeTravel(this) + @currentOffset = 0 + $(document).scroll (e)=> + @documentScroll(e) + @documentScroll pageY: $(document).scrollTop() + + showDay: (date)-> + day = @container.find(".day.#{date}") + @container.find(".day.selected").removeClass('selected') + day.addClass "selected" + offset = day.offset().left + parentOffset = @currentOffset + @container.find(".right").offset().left + @scrollTo parentOffset - offset + + currentOffset: -> + @container.find(".right .inner").offset().left + + top: -> + @_top ||= @container.find('.days').offset().top - 80 + bottom: -> + @_bottom ||= @top() + @container.height() - 50 + + prevPage: -> + @scrollTo @currentOffset + @container.find(".right").width() + + nextPage: -> + @scrollTo @currentOffset - @container.find(".right").width() + + minOffset: -> + @_minOffset ||= @container.find(".right").width() - @container.find(".right .line").width() + @_minOffset + + scrollTo: (offset)-> + @currentOffset = offset + @currentOffset = Math.max(@currentOffset, @minOffset()) + @currentOffset = Math.min(@currentOffset, 0) + @container.find(".right .inner .lines").css "margin-left": "#{@currentOffset}px" + @container.find(".head .week:first-child").css "margin-left", "#{@currentOffset}px" + @timeTravel.scrolledTo 1 - (@minOffset() - @currentOffset) / @minOffset() + setTimeout => + @movePeriodTitles() + , 600 + + movePeriodTitles: -> + @_right_offset ||= @container.find('.right').offset().left + @container.find(".shifted").removeClass("shifted").css "margin-left", 0 + @container.find(".right .line").each (i, l) => + $(l).find(".period").each (i, _p) => + p = $(_p) + offset = parseInt(p.css("left")) + @currentOffset + if offset < 0 && - offset < p.width() + offset = Math.min(-offset, p.width() - 100) + p.find(".title").addClass("shifted").css "margin-left", offset + "px" + return + + documentScroll: (e)-> + if @sticky + if e.pageY < @top() || e.pageY > @bottom() + @container.removeClass "sticky" + @sticky = false + else + if e.pageY > @top() && e.pageY < @bottom() + @sticky = true + @container.addClass "sticky" + + + +export default ReferentialOverview diff --git a/app/javascript/routes/components/App.js b/app/javascript/routes/components/App.js index 0f5786407..26e69bf53 100644 --- a/app/javascript/routes/components/App.js +++ b/app/javascript/routes/components/App.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import AddStopPoint from '../containers/AddStopPoint' import VisibleStopPoints from'../containers/VisibleStopPoints' import clone from '../../helpers/clone' @@ -16,8 +17,8 @@ export default class App extends Component { <VisibleStopPoints /> <AddStopPoint /> </div> - ) - } + ) + } } App.childContextTypes = { diff --git a/app/javascript/routes/components/BSelect2.js b/app/javascript/routes/components/BSelect2.js index 340d9df95..035bce155 100644 --- a/app/javascript/routes/components/BSelect2.js +++ b/app/javascript/routes/components/BSelect2.js @@ -1,6 +1,7 @@ import _ from'lodash' -import React, { Component, PropTypes } from 'react' -import Select2 from 'react-select2' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Select2 from 'react-select2-wrapper' // get JSON full path @@ -84,7 +85,7 @@ class BSelect2 extends Component{ onSelect={ this.props.onSelect } ref='newSelect' options={{ - placeholder: this.context.I18n.routes.edit.select2.placeholder, + placeholder: this.context.I18n.t("routes.edit.select2.placeholder"), allowClear: true, language: 'fr', /* Doesn't seem to work... :( */ theme: 'bootstrap', @@ -96,17 +97,26 @@ class BSelect2 extends Component{ data: function(params) { return { q: params.term, - target_type: 'zdep' + scope: 'route_editor' }; }, processResults: function(data, params) { return { - results: data.map( - item => _.assign( - {}, - item, - { text: item.name + ", " + item.zip_code + " " + item.short_city_name + " <small><em>(" + item.user_objectid + ")</em></small>" } - ) + results: data.map( + function(item) { + var text = item.name; + if (item.zip_code || item.short_city_name) { + text += "," + } + if (item.zip_code) { + text += ` ${item.zip_code}` + } + if (item.short_city_name) { + text += ` ${item.short_city_name}` + } + text += ` <small><em>(${item.area_type.toUpperCase()}, ${item.user_objectid})</em></small>`; + return _.assign({}, item, { text: text }); + } ) }; }, diff --git a/app/javascript/routes/components/OlMap.js b/app/javascript/routes/components/OlMap.js index 2c01dfa7f..4beb02872 100644 --- a/app/javascript/routes/components/OlMap.js +++ b/app/javascript/routes/components/OlMap.js @@ -1,5 +1,6 @@ import _ from 'lodash' -import React, { Component, PropTypes } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' export default class OlMap extends Component{ constructor(props, context){ @@ -114,40 +115,40 @@ export default class OlMap extends Component{ <strong>{this.props.value.olMap.json.name}</strong> </p> <p> - <strong>{this.context.I18n.routes.edit.stop_point_type} : </strong> + <strong>{this.context.I18n.t('routes.edit.map.stop_point_type')} : </strong> {this.props.value.olMap.json.area_type} </p> <p> - <strong>{this.context.I18n.routes.edit.short_name} : </strong> + <strong>{this.context.I18n.t('routes.edit.map.short_name')} : </strong> {this.props.value.olMap.json.short_name} </p> <p> - <strong>{this.context.I18n.id_reflex} : </strong> + <strong>{this.context.I18n.t('id_reflex')} : </strong> {this.props.value.olMap.json.user_objectid} </p> - <p><strong>{this.context.I18n.routes.edit.map.coordinates} : </strong></p> + <p><strong>{this.context.I18n.t('routes.edit.map.coordinates')} : </strong></p> <p style={{paddingLeft: 10, marginTop: 0}}> - <em>{this.context.I18n.routes.edit.map.proj}.: </em>WSG84<br/> - <em>{this.context.I18n.routes.edit.map.lat}.: </em>{this.props.value.olMap.json.latitude} <br/> - <em>{this.context.I18n.routes.edit.map.lon}.: </em>{this.props.value.olMap.json.longitude} + <em>{this.context.I18n.t('routes.edit.map.proj')}.: </em>WSG84<br/> + <em>{this.context.I18n.t('routes.edit.map.lat')}.: </em>{this.props.value.olMap.json.latitude} <br/> + <em>{this.context.I18n.t('routes.edit.map.lon')}.: </em>{this.props.value.olMap.json.longitude} </p> <p> - <strong>{this.context.I18n.routes.edit.map.postal_code} : </strong> + <strong>{this.context.I18n.t('routes.edit.map.postal_code')} : </strong> {this.props.value.olMap.json.zip_code} </p> <p> - <strong>{this.context.I18n.routes.edit.map.city} : </strong> + <strong>{this.context.I18n.t('routes.edit.map.city')} : </strong> {this.props.value.olMap.json.city_name} </p> <p> - <strong>{this.context.I18n.routes.edit.map.comment} : </strong> + <strong>{this.context.I18n.t('routes.edit.map.comment')} : </strong> {this.props.value.olMap.json.comment} </p> {(this.props.value.stoparea_id != this.props.value.olMap.json.stoparea_id) &&( <div className='btn btn-outline-primary btn-sm' onClick= {() => {this.props.onUpdateViaOlMap(this.props.index, this.props.value.olMap.json)}} - >{this.context.I18n.actions.select}</div> + >{this.context.I18n.t('actions.select')}</div> )} </div> <div className='map_content'> @@ -161,7 +162,7 @@ export default class OlMap extends Component{ } } -OlMap.PropTypes = { +OlMap.propTypes = { } OlMap.contextTypes = { diff --git a/app/javascript/routes/components/StopPoint.js b/app/javascript/routes/components/StopPoint.js index 606121f99..af51a6bb4 100644 --- a/app/javascript/routes/components/StopPoint.js +++ b/app/javascript/routes/components/StopPoint.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import BSelect2 from './BSelect2' import OlMap from './OlMap' @@ -16,15 +18,15 @@ export default function StopPoint(props, {I18n}) { <div> <select className='form-control' value={props.value.for_boarding} id="for_boarding" onChange={props.onSelectChange}> - <option value="normal">{I18n.routes.edit.stop_point.boarding.normal}</option> - <option value="forbidden">{I18n.routes.edit.stop_point.boarding.forbidden}</option> + <option value="normal">{I18n.t('routes.edit.stop_point.boarding.normal')}</option> + <option value="forbidden">{I18n.t('routes.edit.stop_point.boarding.forbidden')}</option> </select> </div> <div> <select className='form-control' value={props.value.for_alighting} id="for_alighting" onChange={props.onSelectChange}> - <option value="normal">{I18n.routes.edit.stop_point.alighting.normal}</option> - <option value="forbidden">{I18n.routes.edit.stop_point.alighting.forbidden}</option> + <option value="normal">{I18n.t('routes.edit.stop_point.alighting.normal')}</option> + <option value="forbidden">{I18n.t('routes.edit.stop_point.alighting.forbidden')}</option> </select> </div> @@ -75,7 +77,7 @@ export default function StopPoint(props, {I18n}) { ) } -StopPoint.PropTypes = { +StopPoint.propTypes = { onToggleMap: PropTypes.func.isRequired, onToggleEdit: PropTypes.func.isRequired, onDeleteClick: PropTypes.func.isRequired, @@ -91,4 +93,4 @@ StopPoint.PropTypes = { StopPoint.contextTypes = { I18n: PropTypes.object -}
\ No newline at end of file +} diff --git a/app/javascript/routes/components/StopPointList.js b/app/javascript/routes/components/StopPointList.js index 68af16f57..b227abdea 100644 --- a/app/javascript/routes/components/StopPointList.js +++ b/app/javascript/routes/components/StopPointList.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import StopPoint from './StopPoint' export default function StopPointList({ stopPoints, onDeleteClick, onMoveUpClick, onMoveDownClick, onChange, onSelectChange, onToggleMap, onToggleEdit, onSelectMarker, onUnselectMarker, onUpdateViaOlMap }, {I18n}) { @@ -8,22 +10,22 @@ export default function StopPointList({ stopPoints, onDeleteClick, onMoveUpClick <div className="wrapper"> <div style={{width: 100}}> <div className="form-group"> - <label className="control-label">{I18n.reflex_id}</label> + <label className="control-label">{I18n.t('simple_form.labels.stop_point.reflex_id')}</label> </div> </div> <div> <div className="form-group"> - <label className="control-label">{I18n.simple_form.labels.stop_point.name}</label> + <label className="control-label">{I18n.t('simple_form.labels.stop_point.name')}</label> </div> </div> <div> <div className="form-group"> - <label className="control-label">{I18n.simple_form.labels.stop_point.for_boarding}</label> + <label className="control-label">{I18n.t('simple_form.labels.stop_point.for_boarding')}</label> </div> </div> <div> <div className="form-group"> - <label className="control-label">{I18n.simple_form.labels.stop_point.for_alighting}</label> + <label className="control-label">{I18n.t('simple_form.labels.stop_point.for_alighting')}</label> </div> </div> <div className='actions-5'></div> @@ -54,7 +56,7 @@ export default function StopPointList({ stopPoints, onDeleteClick, onMoveUpClick ) } -StopPointList.PropTypes = { +StopPointList.propTypes = { stopPoints: PropTypes.array.isRequired, onDeleteClick: PropTypes.func.isRequired, onMoveUpClick: PropTypes.func.isRequired, @@ -66,4 +68,4 @@ StopPointList.PropTypes = { StopPointList.contextTypes = { I18n: PropTypes.object -}
\ No newline at end of file +} diff --git a/app/javascript/routes/form_helper.js b/app/javascript/routes/form_helper.js index 8a3277234..865722fb6 100644 --- a/app/javascript/routes/form_helper.js +++ b/app/javascript/routes/form_helper.js @@ -7,14 +7,14 @@ const formHelper = { input.setAttribute('name', formatedName) input.setAttribute('value', value) form.appendChild(input) - }, + }, addError: (ids) => { ids.forEach((id) => { if (!$(id).parents('.form-group').hasClass('has-error')) { $(id).parents('.form-group').addClass('has-error') $(id).parent().append(`<span class='help-block small'>${'doit être rempli(e)'}</span>`) } - }) + }) }, cleanInputs: (ids) => { ids.forEach((id) =>{ @@ -28,21 +28,22 @@ const formHelper = { ids.forEach(id => { $(id).val() == "" ? blankInputs.push(id) : filledInputs.push(id) }) - + if (filledInputs.length > 0) formHelper.cleanInputs(filledInputs) - if (blankInputs.length > 0) formHelper.addError(blankInputs) + if (blankInputs.length > 0) formHelper.addError(blankInputs) }, handleStopPoints: (event, state) => { if (state.stopPoints.length >= 2) { state.stopPoints.map((stopPoint, i) => { formHelper.addInput('id', stopPoint.stoppoint_id ? stopPoint.stoppoint_id : '', i) formHelper.addInput('stop_area_id', stopPoint.stoparea_id, i) - formHelper.addInput('position', i, i) + formHelper.addInput('position', stopPoint.index, i) formHelper.addInput('for_boarding', stopPoint.for_boarding, i) formHelper.addInput('for_alighting', stopPoint.for_alighting, i) }) - if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove() - } else { + if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove() + } + else { event.preventDefault() let msg = "L'itinéraire doit comporter au moins deux arrêts" if ($('.alert.alert-danger').length == 0) { @@ -52,4 +53,4 @@ const formHelper = { } } -export default formHelper
\ No newline at end of file +export default formHelper diff --git a/app/javascript/routes/reducers/stopPoints.js b/app/javascript/routes/reducers/stopPoints.js index eeec06327..0b42b504f 100644 --- a/app/javascript/routes/reducers/stopPoints.js +++ b/app/javascript/routes/reducers/stopPoints.js @@ -38,15 +38,15 @@ const stopPoints = (state = [], action) => { case 'MOVE_STOP_UP': return [ ...state.slice(0, action.index - 1), - _.assign({}, state[action.index], { stoppoint_id: state[action.index - 1].stoppoint_id }), - _.assign({}, state[action.index - 1], { stoppoint_id: state[action.index].stoppoint_id }), + _.assign({}, state[action.index], { index: action.index - 1 }), + _.assign({}, state[action.index - 1], { index: action.index }), ...state.slice(action.index + 1) ] case 'MOVE_STOP_DOWN': return [ ...state.slice(0, action.index), - _.assign({}, state[action.index + 1], { stoppoint_id: state[action.index].stoppoint_id }), - _.assign({}, state[action.index], { stoppoint_id: state[action.index + 1].stoppoint_id }), + _.assign({}, state[action.index + 1], { index: action.index }), + _.assign({}, state[action.index], { index: action.index + 1 }), ...state.slice(action.index + 2) ] case 'DELETE_STOP': @@ -141,4 +141,4 @@ const stopPoints = (state = [], action) => { } } -export default stopPoints
\ No newline at end of file +export default stopPoints diff --git a/app/javascript/time_tables/actions/index.js b/app/javascript/time_tables/actions/index.js index 13cb96b64..98b9eab4b 100644 --- a/app/javascript/time_tables/actions/index.js +++ b/app/javascript/time_tables/actions/index.js @@ -1,5 +1,5 @@ -import range from 'lodash/range' import assign from 'lodash/assign' +import range from 'lodash/range' import reject from 'lodash/reject' import some from 'lodash/some' import every from 'lodash/every' @@ -8,7 +8,7 @@ const I18n = clone(window, "I18n") const actions = { weekDays: (index) => { - return range(1, 8).map(n => I18n.time_tables.edit.metas.days[n]) + return range(1, 8).map(n => I18n.t('time_tables.edit.metas.days')[n]) }, strToArrayDayTypes: (str) =>{ return actions.weekDays().map(day => str.indexOf(day) !== -1) @@ -155,9 +155,9 @@ const actions = { type : 'CLOSE_MODAL' }), monthName(strDate) { - let monthList = range(1,13).map(n => I18n.calendars.months[n]) + let monthList = range(1,13).map(n => I18n.t('calendars.months.'+ n )) let date = new Date(strDate) - return monthList[date.getMonth()] + return monthList[date.getUTCMonth()] }, getHumanDate(strDate, mLimit) { let origin = strDate.split('-') @@ -173,7 +173,7 @@ const actions = { }, getLocaleDate(strDate) { let date = new Date(strDate) - return date.toLocaleDateString() + return date.toLocaleDateString(undefined, { timeZone: 'UTC' }) }, updateSynthesis: ({current_month, time_table_dates: dates, time_table_periods: periods}) => { let newPeriods = reject(periods, 'deleted') @@ -194,7 +194,7 @@ const actions = { for (let period of periods) { let begin = new Date(period.period_start) - let end = new Date(period.period_end) + let end = new Date(period.period_end) if (date >= begin && date <= end) return true } @@ -225,7 +225,7 @@ const actions = { let period = periods[i] if (index !== i && !period.deleted) { if (new Date(period.period_start) <= end && new Date(period.period_end) >= start) { - error = I18n.time_tables.edit.error_submit.periods_overlaps + error = I18n.t('time_tables.edit.error_submit.periods_overlaps') break } } @@ -239,14 +239,15 @@ const actions = { for (let day of in_days) { if (start <= new Date(day.date) && end >= new Date(day.date)) { - error = I18n.time_tables.edit.error_submit.dates_overlaps + error = I18n.t('time_tables.edit.error_submit.dates_overlaps') break } } return error }, fetchTimeTables: (dispatch, nextPage) => { - let urlJSON = window.location.pathname.split('/', 5).join('/') + let urlJSON = window.timetablesUrl || window.location.pathname.split('/', 5).join('/') + if(nextPage) { urlJSON += "/month.json?date=" + nextPage }else{ @@ -277,7 +278,7 @@ const actions = { let strDayTypes = actions.arrayToStrDayTypes(metas.day_types) metas.day_types = strDayTypes let sentState = assign({}, timetable, metas) - let urlJSON = window.location.pathname.split('/', 5).join('/') + let urlJSON = window.timetablesUrl || window.location.pathname.split('/', 5).join('/') let hasError = false fetch(urlJSON + '.json', { credentials: 'same-origin', @@ -315,9 +316,9 @@ const actions = { errorModalMessage: (errorKey) => { switch (errorKey) { case "withoutPeriodsWithDaysTypes": - return I18n.time_tables.edit.error_modal.withoutPeriodsWithDaysTypes + return I18n.t('time_tables.edit.error_modal.withoutPeriodsWithDaysTypes') case "withPeriodsWithoutDayTypes": - return I18n.time_tables.edit.error_modal.withPeriodsWithoutDayTypes + return I18n.t('time_tables.edit.error_modal.withPeriodsWithoutDayTypes') default: return errorKey @@ -325,4 +326,4 @@ const actions = { } } -export default actions
\ No newline at end of file +export default actions diff --git a/app/javascript/time_tables/components/ConfirmModal.js b/app/javascript/time_tables/components/ConfirmModal.js index d89170ee7..4e8583bc0 100644 --- a/app/javascript/time_tables/components/ConfirmModal.js +++ b/app/javascript/time_tables/components/ConfirmModal.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, timetable, metas}, {I18n}) { return ( @@ -7,11 +9,11 @@ export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCan <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <h4 className='modal-title'>{I18n.time_tables.edit.confirm_modal.title}</h4> + <h4 className='modal-title'>{I18n.t('time_tables.edit.confirm_modal.title')}</h4> </div> <div className='modal-body'> <div className='mt-md mb-md'> - <p>{I18n.time_tables.edit.confirm_modal.message}</p> + <p>{I18n.t('time_tables.edit.confirm_modal.message')}</p> </div> </div> <div className='modal-footer'> @@ -21,7 +23,7 @@ export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCan type='button' onClick={() => { onModalCancel(modal.confirmModal.callback) }} > - {I18n.cancel} + {I18n.t('cancel')} </button> <button className='btn btn-primary' @@ -29,7 +31,7 @@ export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCan type='button' onClick={() => { onModalAccept(modal.confirmModal.callback, timetable, metas) }} > - {I18n.actions.submit} + {I18n.t('actions.submit')} </button> </div> </div> @@ -47,4 +49,4 @@ ConfirmModal.propTypes = { ConfirmModal.contextTypes = { I18n: PropTypes.object -}
\ No newline at end of file +} diff --git a/app/javascript/time_tables/components/ErrorModal.js b/app/javascript/time_tables/components/ErrorModal.js index e810f49ab..8af12f1d1 100644 --- a/app/javascript/time_tables/components/ErrorModal.js +++ b/app/javascript/time_tables/components/ErrorModal.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import actions from '../actions' export default function ErrorModal({dispatch, modal, onModalClose}, {I18n}) { @@ -8,7 +10,7 @@ export default function ErrorModal({dispatch, modal, onModalClose}, {I18n}) { <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <h4 className='modal-title'>{I18n.time_tables.edit.error_modal.title}</h4> + <h4 className='modal-title'>{I18n.t('time_tables.edit.error_modal.title')}</h4> </div> <div className='modal-body'> <div className='mt-md mb-md'> @@ -22,7 +24,7 @@ export default function ErrorModal({dispatch, modal, onModalClose}, {I18n}) { type='button' onClick={() => { onModalClose() }} > - {I18n.back} + {I18n.t('back')} </button> </div> </div> @@ -39,4 +41,4 @@ ErrorModal.propTypes = { ErrorModal.contextTypes = { I18n: PropTypes.object -}
\ No newline at end of file +} diff --git a/app/javascript/time_tables/components/ExceptionsInDay.js b/app/javascript/time_tables/components/ExceptionsInDay.js index 3335ee89d..f5ed625be 100644 --- a/app/javascript/time_tables/components/ExceptionsInDay.js +++ b/app/javascript/time_tables/components/ExceptionsInDay.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class ExceptionsInDay extends Component { diff --git a/app/javascript/time_tables/components/Metas.js b/app/javascript/time_tables/components/Metas.js index 7098d2b82..08a6e26fe 100644 --- a/app/javascript/time_tables/components/Metas.js +++ b/app/javascript/time_tables/components/Metas.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import actions from '../actions' import TagsSelect2 from './TagsSelect2' @@ -11,7 +13,7 @@ export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdat {/* comment (name) */} <div className="form-group"> <label htmlFor="" className="control-label col-sm-4 required"> - {I18n.time_tables.edit.metas.name} <abbr title="">*</abbr> + {I18n.t('time_tables.edit.metas.name')} <abbr title="">*</abbr> </label> <div className="col-sm-8"> <input @@ -25,8 +27,8 @@ export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdat </div> {/* color */} - <div className="form-group"> - <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.color}</label> + {metas.color !== undefined && <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.attribute_name('time_table', 'color')}</label> <div className="col-sm-8"> <div className="dropdown color_selector"> <button @@ -67,11 +69,11 @@ export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdat </div> </div> </div> - </div> + </div>} {/* tags */} - <div className="form-group"> - <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.tag_list}</label> + {metas.tags !== undefined && <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.attribute_name('time_table', 'tag_list')}</label> <div className="col-sm-8"> <TagsSelect2 initialTags={metas.initial_tags} @@ -80,20 +82,20 @@ export default function Metas({metas, onUpdateDayTypes, onUpdateComment, onUpdat onUnselect2Tags={(e) => onUnselect2Tags(e)} /> </div> - </div> + </div>} {/* calendar */} - <div className="form-group"> - <label htmlFor="" className="control-label col-sm-4">{I18n.activerecord.attributes.time_table.calendar}</label> + {metas.calendar !== null && <div className="form-group"> + <label htmlFor="" className="control-label col-sm-4">{I18n.attribute_name('time_table', 'calendar')}</label> <div className="col-sm-8"> - <span>{metas.calendar ? metas.calendar.name : I18n.time_tables.edit.metas.no_calendar}</span> + <span>{metas.calendar ? metas.calendar.name : I18n.t('time_tables.edit.metas.no_calendar')}</span> </div> - </div> + </div>} {/* day_types */} <div className="form-group"> <label htmlFor="" className="control-label col-sm-4"> - {I18n.time_tables.edit.metas.day_types} + {I18n.t('time_tables.edit.metas.day_types')} </label> <div className="col-sm-8"> <div className="form-group labelled-checkbox-group"> diff --git a/app/javascript/time_tables/components/Navigate.js b/app/javascript/time_tables/components/Navigate.js index 7307d819b..1467fffe9 100644 --- a/app/javascript/time_tables/components/Navigate.js +++ b/app/javascript/time_tables/components/Navigate.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import map from 'lodash/map' import actions from '../actions' @@ -23,7 +24,7 @@ export default function Navigate({ dispatch, metas, timetable, pagination, statu aria-haspopup='true' aria-expanded='true' > - {pagination.currentPage ? (actions.monthName(pagination.currentPage) + ' ' + new Date(pagination.currentPage).getFullYear()) : ''} + {pagination.currentPage ? (actions.monthName(pagination.currentPage) + ' ' + new Date(pagination.currentPage).getUTCFullYear()) : ''} <span className='caret'></span> </div> <ul @@ -40,7 +41,7 @@ export default function Navigate({ dispatch, metas, timetable, pagination, statu dispatch(actions.checkConfirmModal(e, actions.changePage(dispatch, e.currentTarget.value), pagination.stateChanged, dispatch, metas, timetable)) }} > - {actions.monthName(month) + ' ' + new Date(month).getFullYear()} + {actions.monthName(month) + ' ' + new Date(month).getUTCFullYear()} </button> </li> ))} @@ -85,4 +86,4 @@ Navigate.propTypes = { status: PropTypes.object.isRequired, pagination: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/time_tables/components/PeriodForm.js b/app/javascript/time_tables/components/PeriodForm.js index d9f1d3437..d17a246f7 100644 --- a/app/javascript/time_tables/components/PeriodForm.js +++ b/app/javascript/time_tables/components/PeriodForm.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import filter from 'lodash/filter' let monthsArray = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'] @@ -44,7 +46,7 @@ export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm <div> <div className="form-group"> <label htmlFor="" className="control-label required"> - {I18n.time_tables.edit.period_form.begin} + {I18n.t('time_tables.edit.period_form.begin')} <abbr title="requis">*</abbr> </label> </div> @@ -52,7 +54,7 @@ export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm <div> <div className="form-group"> <label htmlFor="" className="control-label required"> - {I18n.time_tables.edit.period_form.end} + {I18n.t('time_tables.edit.period_form.end')} <abbr title="requis">*</abbr> </label> </div> @@ -103,14 +105,14 @@ export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm className='btn btn-link' onClick={onClosePeriodForm} > - {I18n.cancel} + {I18n.t('cancel')} </button> <button type='button' className='btn btn-outline-primary mr-sm' onClick={() => onValidatePeriodForm(modal.modalProps, timetable.time_table_periods, metas, filter(timetable.time_table_dates, ['in_out', true]))} > - {I18n.actions.submit} + {I18n.t('actions.submit')} </button> </div> </div> @@ -122,7 +124,7 @@ export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm className='btn btn-outline-primary' onClick={onOpenAddPeriodForm} > - {I18n.time_tables.actions.add_period} + {I18n.t('time_tables.actions.add_period')} </button> </div> } @@ -130,7 +132,7 @@ export default function PeriodForm({modal, timetable, metas, onOpenAddPeriodForm </div> </div> </div> - ) + ) } PeriodForm.propTypes = { @@ -145,4 +147,4 @@ PeriodForm.propTypes = { PeriodForm.contextTypes = { I18n: PropTypes.object -}
\ No newline at end of file +} diff --git a/app/javascript/time_tables/components/PeriodManager.js b/app/javascript/time_tables/components/PeriodManager.js index 9922ce2c4..6b817fe73 100644 --- a/app/javascript/time_tables/components/PeriodManager.js +++ b/app/javascript/time_tables/components/PeriodManager.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class PeriodManager extends Component { diff --git a/app/javascript/time_tables/components/PeriodsInDay.js b/app/javascript/time_tables/components/PeriodsInDay.js index 888537579..1aed5c969 100644 --- a/app/javascript/time_tables/components/PeriodsInDay.js +++ b/app/javascript/time_tables/components/PeriodsInDay.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import PeriodManager from './PeriodManager' export default class PeriodsInDay extends Component { diff --git a/app/javascript/time_tables/components/SaveTimetable.js b/app/javascript/time_tables/components/SaveTimetable.js index d5a57bd1c..704590abd 100644 --- a/app/javascript/time_tables/components/SaveTimetable.js +++ b/app/javascript/time_tables/components/SaveTimetable.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' export default class SaveTimetable extends Component{ diff --git a/app/javascript/time_tables/components/TagsSelect2.js b/app/javascript/time_tables/components/TagsSelect2.js index 70a748a04..43cf59fdf 100644 --- a/app/javascript/time_tables/components/TagsSelect2.js +++ b/app/javascript/time_tables/components/TagsSelect2.js @@ -1,9 +1,10 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import mapKeys from 'lodash/mapKeys' import map from 'lodash/map' import filter from 'lodash/filter' import assign from 'lodash/assign' -import Select2 from 'react-select2' +import Select2 from 'react-select2-wrapper' // get JSON full path let origin = window.location.origin @@ -39,7 +40,7 @@ export default class TagsSelect2 extends Component { allowClear: true, theme: 'bootstrap', width: '100%', - placeholder: this.context.I18n.time_tables.edit.select2.tag.placeholder, + placeholder: this.context.I18n.t('time_tables.edit.select2.tag.placeholder'), ajax: { url: origin + path + '/tags.json', dataType: 'json', diff --git a/app/javascript/time_tables/components/TimeTableDay.js b/app/javascript/time_tables/components/TimeTableDay.js index 165c7b848..498e7d0cd 100644 --- a/app/javascript/time_tables/components/TimeTableDay.js +++ b/app/javascript/time_tables/components/TimeTableDay.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' export default class TimeTableDay extends Component { constructor(props) { diff --git a/app/javascript/time_tables/components/Timetable.js b/app/javascript/time_tables/components/Timetable.js index df6e6016b..991f31435 100644 --- a/app/javascript/time_tables/components/Timetable.js +++ b/app/javascript/time_tables/components/Timetable.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' import TimeTableDay from './TimeTableDay' import PeriodsInDay from './PeriodsInDay' @@ -30,11 +31,11 @@ export default class Timetable extends Component { <div className="table table-2entries mb-sm"> <div className="t2e-head w20"> <div className="th"> - <div className="strong">{this.context.I18n.time_tables.synthesis}</div> + <div className="strong">{this.context.I18n.t('time_tables.edit.synthesis')}</div> </div> - <div className="td"><span>{this.context.I18n.time_tables.edit.day_types}</span></div> - <div className="td"><span>{this.context.I18n.time_tables.edit.periods}</span></div> - <div className="td"><span>{this.context.I18n.time_tables.edit.exceptions}</span></div> + <div className="td"><span>{this.context.I18n.t('time_tables.edit.day_types')}</span></div> + <div className="td"><span>{this.context.I18n.t('time_tables.edit.periods')}</span></div> + <div className="td"><span>{this.context.I18n.t('time_tables.edit.exceptions')}</span></div> </div> <div className="t2e-item-list w80"> <div> diff --git a/app/javascript/time_tables/containers/App.js b/app/javascript/time_tables/containers/App.js index 235dccb50..5963f8f1d 100644 --- a/app/javascript/time_tables/containers/App.js +++ b/app/javascript/time_tables/containers/App.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import { connect } from'react-redux' import actions from '../actions' import Metas from './Metas' diff --git a/app/javascript/vehicle_journeys/actions/index.js b/app/javascript/vehicle_journeys/actions/index.js index ce4b9209d..4ca8bd73b 100644 --- a/app/javascript/vehicle_journeys/actions/index.js +++ b/app/javascript/vehicle_journeys/actions/index.js @@ -13,8 +13,8 @@ const actions = { exitEditMode: () => ({ type: "EXIT_EDIT_MODE" }), - receiveVehicleJourneys : (json) => ({ - type: "RECEIVE_VEHICLE_JOURNEYS", + receiveVehicleJourneys : (json, returnJourneys) => ({ + type: (returnJourneys ? "RECEIVE_RETURN_VEHICLE_JOURNEYS" : "RECEIVE_VEHICLE_JOURNEYS"), json }), receiveErrors : (json) => ({ @@ -57,15 +57,22 @@ const actions = { selectedItem: { id: selectedJP.id, objectid: selectedJP.object_id, + short_id: selectedJP.short_id, name: selectedJP.name, published_name: selectedJP.published_name, - stop_areas: selectedJP.stop_area_short_descriptions + stop_areas: selectedJP.stop_area_short_descriptions, + costs: selectedJP.costs, + full_schedule: selectedJP.full_schedule } }), openEditModal : (vehicleJourney) => ({ type : 'EDIT_VEHICLEJOURNEY_MODAL', vehicleJourney }), + openInfoModal : (vehicleJourney) => ({ + type : 'INFO_VEHICLEJOURNEY_MODAL', + vehicleJourney + }), openNotesEditModal : (vehicleJourney) => ({ type : 'EDIT_NOTES_VEHICLEJOURNEY_MODAL', vehicleJourney @@ -84,7 +91,8 @@ const actions = { selectedItem:{ id: selectedTT.id, comment: selectedTT.comment, - objectid: selectedTT.objectid + objectid: selectedTT.objectid, + color: selectedTT.color } }), addSelectedTimetable: () => ({ @@ -99,6 +107,31 @@ const actions = { vehicleJourneys, timetables }), + openPurchaseWindowsEditModal : (vehicleJourneys) => ({ + type : 'EDIT_PURCHASE_WINDOWS_VEHICLEJOURNEY_MODAL', + vehicleJourneys + }), + selectPurchaseWindowsModal: (selectedWindow) =>({ + type: 'SELECT_PURCHASE_WINDOW_MODAL', + selectedItem:{ + id: selectedWindow.id, + name: selectedWindow.name, + color: selectedWindow.color, + objectid: selectedWindow.objectid + } + }), + addSelectedPurchaseWindow: () => ({ + type: 'ADD_SELECTED_PURCHASE_WINDOW' + }), + deletePurchaseWindowsModal : (purchaseWindow) => ({ + type : 'DELETE_PURCHASE_WINDOW_MODAL', + purchaseWindow + }), + editVehicleJourneyPurchaseWindows : (vehicleJourneys, purchase_windows) => ({ + type: 'EDIT_VEHICLEJOURNEYS_PURCHASE_WINDOWS', + vehicleJourneys, + purchase_windows + }), openShiftModal : () => ({ type : 'SHIFT_VEHICLEJOURNEY_MODAL' }), @@ -162,26 +195,18 @@ const actions = { resetValidation: (target) => { $(target).parent().removeClass('has-error').children('.help-block').remove() }, - validateFields : (...fields) => { - const test = [] - - Object.keys(fields).map(function(key) { - test.push(fields[key].validity.valid) + validateFields : (fields) => { + let valid = true + Object.keys(fields).forEach((key) => { + let field = fields[key] + if(field.validity && !field.validity.valid){ + valid = false + $(field).parent().addClass('has-error').children('.help-block').remove() + $(field).parent().append("<span class='small help-block'>" + field.validationMessage + "</span>") + } }) - if(test.indexOf(false) >= 0) { - // Form is invalid - test.map(function(item, i) { - if(item == false) { - const k = Object.keys(fields)[i] - $(fields[k]).parent().addClass('has-error').children('.help-block').remove() - $(fields[k]).parent().append("<span class='small help-block'>" + fields[k].validationMessage + "</span>") - } - }) - return false - } else { - // Form is valid - return true - } + + return valid }, toggleArrivals : () => ({ type: 'TOGGLE_ARRIVALS', @@ -269,10 +294,17 @@ const actions = { type: 'RECEIVE_TOTAL_COUNT', total }), - fetchVehicleJourneys : (dispatch, currentPage, nextPage, queryString) => { + fetchVehicleJourneys : (dispatch, currentPage, nextPage, queryString, url) => { + let returnJourneys = false if(currentPage == undefined){ currentPage = 1 } + if(url == undefined){ + url = window.location.pathname + } + else{ + returnJourneys = true + } let vehicleJourneys = [] let page switch (nextPage) { @@ -294,7 +326,7 @@ const actions = { str = '.json?page=' + page.toString() sep = '&' } - let urlJSON = window.location.pathname + str + let urlJSON = url + str if (queryString){ urlJSON = urlJSON + sep + queryString } @@ -313,6 +345,7 @@ const actions = { let val for (val of json.vehicle_journeys){ var timeTables = [] + var purchaseWindows = [] let tt for (tt of val.time_tables){ timeTables.push({ @@ -322,33 +355,56 @@ const actions = { color: tt.color }) } + if(val.purchase_windows){ + for (tt of val.purchase_windows){ + purchaseWindows.push({ + objectid: tt.objectid, + name: tt.name, + id: tt.id, + color: tt.color + }) + } + } let vjasWithDelta = val.vehicle_journey_at_stops.map((vjas, i) => { actions.fillEmptyFields(vjas) return actions.getDelta(vjas) }) - vehicleJourneys.push({ - journey_pattern: val.journey_pattern, - published_journey_name: val.published_journey_name, - objectid: val.objectid, - short_id: val.short_id, - footnotes: val.footnotes, - time_tables: timeTables, - vehicle_journey_at_stops: vjasWithDelta, - deletable: false, - selected: false, - published_journey_name: val.published_journey_name || 'non renseigné', - published_journey_identifier: val.published_journey_identifier || 'non renseigné', - company: val.company || 'non renseigné', - transport_mode: val.route.line.transport_mode || 'undefined', - transport_submode: val.route.line.transport_submode || 'undefined' - }) + + vehicleJourneys.push( + _.assign({}, val, { + time_tables: timeTables, + purchase_windows: purchaseWindows, + vehicle_journey_at_stops: vjasWithDelta, + deletable: false, + selected: false, + published_journey_name: val.published_journey_name || 'non renseigné', + published_journey_identifier: val.published_journey_identifier || 'non renseigné', + company: val.company || {name: 'non renseigné'}, + transport_mode: val.route.line.transport_mode || 'undefined', + transport_submode: val.route.line.transport_submode || 'undefined' + }) + ) } window.currentItemsLength = vehicleJourneys.length - dispatch(actions.receiveVehicleJourneys(vehicleJourneys)) - dispatch(actions.receiveTotalCount(json.total)) + dispatch(actions.receiveVehicleJourneys(vehicleJourneys, returnJourneys)) + if(!returnJourneys){ + dispatch(actions.receiveTotalCount(json.total)) + } } }) }, + + validate : (dispatch, vehicleJourneys, next) => { + dispatch(actions.didValidateVehicleJourneys(vehicleJourneys)) + actions.submitVehicleJourneys(dispatch, vehicleJourneys, next) + return true + }, + + didValidateVehicleJourneys : (vehicleJourneys) => ({ + type: 'DID_VALIDATE_VEHICLE_JOURNEYS', + vehicleJourneys + }), + submitVehicleJourneys : (dispatch, state, next) => { dispatch(actions.fetchingApi()) let urlJSON = window.location.pathname + "_collection.json" @@ -439,6 +495,20 @@ const actions = { vjas.delta = delta return vjas }, + adjustSchedule: (action, schedule) => { + // we enforce that the departure time remains after the arrival time + actions.getDelta(schedule) + if(schedule.delta < 0){ + if(action.isDeparture){ + schedule.arrival_time = schedule.departure_time + } + else{ + schedule.departure_time = schedule.arrival_time + } + actions.getDelta(schedule) + } + return schedule + }, getShiftedSchedule: ({departure_time, arrival_time}, additional_time) => { // We create dummy dates objects to manipulate time more easily let departureDT = new Date (Date.UTC(2017, 2, 1, parseInt(departure_time.hour), parseInt(departure_time.minute))) diff --git a/app/javascript/vehicle_journeys/components/App.js b/app/javascript/vehicle_journeys/components/App.js index 8e5f7aa9d..5ac284438 100644 --- a/app/javascript/vehicle_journeys/components/App.js +++ b/app/javascript/vehicle_journeys/components/App.js @@ -22,6 +22,7 @@ export default function App() { <Filters /> <VehicleJourneysList /> + {window.returnRouteUrl && <VehicleJourneysList routeUrl={window.returnRouteUrl}/>} <div className='row'> <div className='col-lg-12 text-right'> @@ -35,4 +36,4 @@ export default function App() { <ConfirmModal /> </div> ) -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/ConfirmModal.js b/app/javascript/vehicle_journeys/components/ConfirmModal.js index df3c96c48..75e8a3932 100644 --- a/app/javascript/vehicle_journeys/components/ConfirmModal.js +++ b/app/javascript/vehicle_journeys/components/ConfirmModal.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCancel, vehicleJourneys}) { return ( @@ -6,7 +7,7 @@ export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCan <div className='modal-dialog'> <div className='modal-content'> <div className='modal-body'> - <p> Voulez-vous valider vos modifications avant de changer de page? </p> + <p> {I18n.t('vehicle_journeys.vehicle_journeys_matrix.modal_confirm')} </p> </div> <div className='modal-footer'> <button @@ -30,11 +31,11 @@ export default function ConfirmModal({dispatch, modal, onModalAccept, onModalCan </div> </div> ) -} +} ConfirmModal.propTypes = { vehicleJourneys: PropTypes.array.isRequired, modal: PropTypes.object.isRequired, onModalAccept: PropTypes.func.isRequired, onModalCancel: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/Filters.js b/app/javascript/vehicle_journeys/components/Filters.js index db6707520..f8697c930 100644 --- a/app/javascript/vehicle_journeys/components/Filters.js +++ b/app/javascript/vehicle_journeys/components/Filters.js @@ -1,9 +1,11 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import MissionSelect2 from'./tools/select2s/MissionSelect2' import VJSelect2 from'./tools/select2s/VJSelect2' import TimetableSelect2 from'./tools/select2s/TimetableSelect2' -export default function Filters({filters, pagination, onFilter, onResetFilters, onUpdateStartTimeFilter, onUpdateEndTimeFilter, onToggleWithoutSchedule, onToggleWithoutTimeTable, onSelect2Timetable, onSelect2JourneyPattern, onSelect2VehicleJourney}) { +export default function Filters({filters, pagination, missions, onFilter, onResetFilters, onUpdateStartTimeFilter, onUpdateEndTimeFilter, onToggleWithoutSchedule, onToggleWithoutTimeTable, onSelect2Timetable, onSelect2JourneyPattern, onSelect2VehicleJourney}) { return ( <div className='row'> <div className='col-lg-12'> @@ -24,6 +26,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, onSelect2JourneyPattern={onSelect2JourneyPattern} filters={filters} isFilter={true} + values={missions} /> </div> @@ -33,6 +36,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, onSelect2Timetable={onSelect2Timetable} hasRoute={true} chunkURL={("/autocomplete_time_tables.json?route_id=" + String(window.route_id))} + searchKey={"comment_or_objectid_cont_any"} filters={filters} isFilter={true} /> @@ -42,10 +46,10 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, <div className='ffg-row'> {/* Plage horaire */} <div className='form-group togglable'> - <label className='control-label'>Plage horaire au départ de la course</label> + <label className='control-label'>{I18n.t("vehicle_journeys.form.departure_range.label")}</label> <div className='filter_menu'> <div className='form-group time filter_menu-item'> - <label className='control-label time'>Début</label> + <label className='control-label time'>{I18n.t("vehicle_journeys.form.departure_range.start")}</label> <div className='form-inline'> <div className='input-group time'> <input @@ -69,7 +73,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, </div> </div> <div className='form-group time filter_menu-item'> - <label className='control-label time'>Fin</label> + <label className='control-label time'>{I18n.t("vehicle_journeys.form.departure_range.end")}</label> <div className='form-inline'> <div className='input-group time'> <input @@ -97,7 +101,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, {/* Switch avec/sans horaires */} <div className='form-group has_switch'> - <label className='control-label pull-left'>Afficher les courses sans horaires</label> + <label className='control-label pull-left'>{I18n.t("vehicle_journeys.form.show_journeys_without_schedule")}</label> <div className='form-group pull-left' style={{padding: 0}}> <div className='checkbox'> <label> @@ -106,8 +110,8 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, onChange={onToggleWithoutSchedule} checked={filters.query.withoutSchedule} ></input> - <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'> - {filters.query.withoutSchedule ? 'Oui' : 'Non'} + <span className='switch-label' data-checkedvalue={I18n.t("no")} data-uncheckedvalue={I18n.t("yes")}> + {filters.query.withoutSchedule ? I18n.t("yes") : I18n.t("no")} </span> </label> </div> @@ -118,7 +122,7 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, <div className="ffg-row"> {/* Switch avec/sans calendrier */} <div className='form-group has_switch'> - <label className='control-label pull-left'>Afficher les courses avec calendrier</label> + <label className='control-label pull-left'>{I18n.t("vehicle_journeys.form.show_journeys_with_calendar")}</label> <div className='form-group pull-left' style={{padding: 0}}> <div className='checkbox'> <label> @@ -127,8 +131,8 @@ export default function Filters({filters, pagination, onFilter, onResetFilters, onChange={onToggleWithoutTimeTable} checked={filters.query.withoutTimeTable} ></input> - <span className='switch-label' data-checkedvalue='Non' data-uncheckedvalue='Oui'> - {filters.query.withoutTimeTable ? 'Oui' : 'Non'} + <span className='switch-label' data-checkedvalue={I18n.t("no")} data-uncheckedvalue={I18n.t("yes")}> + {filters.query.withoutTimeTable ? I18n.t("yes") : I18n.t("no")} </span> </label> </div> @@ -165,4 +169,4 @@ Filters.propTypes = { onSelect2Timetable: PropTypes.func.isRequired, onSelect2JourneyPattern: PropTypes.func.isRequired, onSelect2VehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/Navigate.js b/app/javascript/vehicle_journeys/components/Navigate.js index 7493b705b..24843babc 100644 --- a/app/javascript/vehicle_journeys/components/Navigate.js +++ b/app/javascript/vehicle_journeys/components/Navigate.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from'../actions' export default function Navigate({ dispatch, vehicleJourneys, pagination, status, filters}) { @@ -16,8 +17,7 @@ export default function Navigate({ dispatch, vehicleJourneys, pagination, status if(status.fetchSuccess == true) { return ( <div className="pagination"> - Liste des horaires {minVJ} à {maxVJ} sur {pagination.totalCount} - + {I18n.t("vehicle_journeys.vehicle_journeys_matrix.pagination", {minVJ, maxVJ, total:pagination.totalCount})} <form className='page_links' onSubmit={e => {e.preventDefault()}}> <button onClick={e => { @@ -52,4 +52,4 @@ Navigate.propTypes = { status: PropTypes.object.isRequired, pagination: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js index e8c27f92e..fb921df9c 100644 --- a/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/SaveVehicleJourneys.js @@ -1,34 +1,19 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import SaveButton from '../../helpers/save_button' import actions from '../actions' -export default class SaveVehicleJourneys extends Component{ - constructor(props){ - super(props) +export default class SaveVehicleJourneys extends SaveButton{ + hasPolicy(){ + return this.props.filters.policy['vehicle_journeys.update'] == true } - render() { - if (this.props.filters.policy['vehicle_journeys.update'] == false) { - return false - }else{ - return ( - <div className='row mt-md'> - <div className='col-lg-12 text-right'> - <form className='vehicle_journeys formSubmitr ml-xs' onSubmit={e => {e.preventDefault()}}> - <button - className='btn btn-default' - type='button' - onClick={e => { - e.preventDefault() - this.props.editMode ? this.props.onSubmitVehicleJourneys(this.props.dispatch, this.props.vehicleJourneys) : this.props.onEnterEditMode() - }} - > - {this.props.editMode ? "Valider" : "Editer"} - </button> - </form> - </div> - </div> - ) - } + formClassName(){ + return 'vehicle_journeys' + } + + submitForm(){ + this.props.validate(this.props.vehicleJourneys, this.props.dispatch) } } @@ -38,5 +23,6 @@ SaveVehicleJourneys.propTypes = { status: PropTypes.object.isRequired, filters: PropTypes.object.isRequired, onEnterEditMode: PropTypes.func.isRequired, + onExitEditMode: PropTypes.func.isRequired, onSubmitVehicleJourneys: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/ToggleArrivals.js b/app/javascript/vehicle_journeys/components/ToggleArrivals.js index e26ceec3a..9a2b0097f 100644 --- a/app/javascript/vehicle_journeys/components/ToggleArrivals.js +++ b/app/javascript/vehicle_journeys/components/ToggleArrivals.js @@ -1,9 +1,11 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + export default function ToggleArrivals({filters, onToggleArrivals}) { return ( <div className='has_switch form-group inline'> - <label htmlFor='toggleArrivals' className='control-label'>Afficher et éditer les horaires d'arrivée</label> + <label htmlFor='toggleArrivals' className='control-label'>{I18n.t('vehicle_journeys.form.show_arrival_time')}</label> <div className='form-group'> <div className='checkbox'> <label> @@ -24,4 +26,4 @@ export default function ToggleArrivals({filters, onToggleArrivals}) { ToggleArrivals.propTypes = { filters : PropTypes.object.isRequired, onToggleArrivals: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/Tools.js b/app/javascript/vehicle_journeys/components/Tools.js index 7621dfc10..22ea44283 100644 --- a/app/javascript/vehicle_journeys/components/Tools.js +++ b/app/javascript/vehicle_journeys/components/Tools.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' import AddVehicleJourney from '../containers/tools/AddVehicleJourney' import DeleteVehicleJourneys from '../containers/tools/DeleteVehicleJourneys' @@ -7,6 +8,7 @@ import DuplicateVehicleJourney from '../containers/tools/DuplicateVehicleJourney import EditVehicleJourney from '../containers/tools/EditVehicleJourney' import NotesEditVehicleJourney from '../containers/tools/NotesEditVehicleJourney' import TimetablesEditVehicleJourney from '../containers/tools/TimetablesEditVehicleJourney' +import PurchaseWindowsEditVehicleJourney from '../containers/tools/PurchaseWindowsEditVehicleJourney' export default class Tools extends Component { @@ -17,7 +19,12 @@ export default class Tools extends Component { hasPolicy(key) { // Check if the user has the policy to disable or not the action - return this.props.filters.policy[`vehicle_journeys.${key}`] + return this.props.filters.policy[`vehicle_journeys.${key}`] + } + + hasFeature(key) { + // Check if the organisation has the given feature + return this.props.filters.features[key] } render() { @@ -25,17 +32,20 @@ export default class Tools extends Component { 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}/> + <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")}/> + { this.hasFeature('purchase_windows') && + <PurchaseWindowsEditVehicleJourney disabled={!this.hasPolicy("update")}/> + } <NotesEditVehicleJourney disabled={!this.hasPolicy("update")}/> - <DeleteVehicleJourneys disabled={this.hasPolicy("destroy") && !editMode}/> + <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> + <span className='info-msg'>{I18n.t('vehicle_journeys.vehicle_journeys_matrix.selected_journeys', {count: actions.getSelected(vehicleJourneys).length})}</span> + <button className='btn btn-xs btn-link pull-right' onClick={onCancelSelection}>{I18n.t('vehicle_journeys.vehicle_journeys_matrix.cancel_selection')}</button> </div> ) } @@ -45,4 +55,4 @@ Tools.propTypes = { vehicleJourneys : PropTypes.array.isRequired, onCancelSelection: PropTypes.func.isRequired, filters: PropTypes.object.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/VehicleJourney.js b/app/javascript/vehicle_journeys/components/VehicleJourney.js index 929cbc5c4..4a9432231 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourney.js @@ -1,5 +1,8 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../actions' +import EditVehicleJourney from '../containers/tools/EditVehicleJourney' +import VehicleJourneyInfoButton from '../containers/tools/VehicleJourneyInfoButton' export default class VehicleJourney extends Component { constructor(props) { @@ -8,13 +11,11 @@ export default class VehicleJourney extends Component { } cityNameChecker(sp) { - let bool = false - if(sp.stop_area_cityname != this.previousCity){ - bool = true - this.previousCity = sp.stop_area_cityname - } + return this.props.vehicleJourneys.showHeader(sp.stop_point_objectid) + } - return bool + hasFeature(key) { + return this.props.filters.features[key] } timeTableURL(tt) { @@ -22,7 +23,16 @@ export default class VehicleJourney extends Component { let ttURL = refURL + '/time_tables/' + tt.id return ( - <a href={ttURL} title='Voir le calendrier'><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '')}}></span></a> + <a href={ttURL} title={I18n.t('vehicle_journeys.vehicle_journeys_matrix.show_timetable')}><span className='fa fa-calendar' style={{ color: (tt.color ? tt.color : '#4B4B4B')}}></span></a> + ) + } + + purchaseWindowURL(tt) { + let refURL = window.location.pathname.split('/', 3).join('/') + let ttURL = refURL + '/purchase_windows/' + tt.id + + return ( + <a href={ttURL} title={I18n.t('vehicle_journeys.vehicle_journeys_matrix.show_purchase_window')}><span className='fa fa-calendar' style={{color: (tt.color ? tt.color : '')}}></span></a> ) } @@ -44,20 +54,35 @@ export default class VehicleJourney extends Component { render() { this.previousCity = undefined - let {time_tables} = this.props.value + let {time_tables, purchase_windows} = this.props.value return ( <div className={'t2e-item' + (this.props.value.deletable ? ' disabled' : '') + (this.props.value.errors ? ' has-error': '')}> - <div className='th'> + <div + className='th' + onClick={(e) => + !this.props.disabled && ($(e.target).parents("a").length == 0) && this.props.onSelectVehicleJourney(this.props.index) + } + > <div className='strong mb-xs'>{this.props.value.short_id || '-'}</div> + <div>{this.props.value.published_journey_name && this.props.value.published_journey_name != I18n.t('undefined') ? this.props.value.published_journey_name : '-'}</div> <div>{this.props.value.journey_pattern.short_id || '-'}</div> + <div>{this.props.value.company ? this.props.value.company.name : '-'}</div> <div> {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'}> + { this.hasFeature('purchase_windows') && + <div> + {purchase_windows.slice(0,3).map((tt, i)=> + <span key={i} className='vj_tt'>{this.purchaseWindowURL(tt)}</span> + )} + {purchase_windows.length > 3 && <span className='vj_tt'> + {purchase_windows.length - 3}</span>} + </div> + } + {!this.props.disabled && <div className={(this.props.value.deletable ? 'disabled ' : '') + 'checkbox'}> <input id={this.props.index} name={this.props.index} @@ -68,20 +93,21 @@ export default class VehicleJourney extends Component { checked={this.props.value.selected} ></input> <label htmlFor={this.props.index}></label> - </div> + </div>} + {this.props.disabled && <VehicleJourneyInfoButton vehicleJourney={this.props.value} />} </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 à'> + <div data-headline={I18n.t("vehicle_journeys.form.arrival_at")}> <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} + disabled={!this.props.editMode || 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']} @@ -92,7 +118,7 @@ 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} + disabled={!this.props.editMode || 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']} @@ -105,14 +131,14 @@ export default class VehicleJourney extends Component { <span className='sb sb-chrono sb-lg text-warning' data-textinside={vj.delta}></span> } </div> - <div data-headline='Départ à'> + <div data-headline={I18n.t("vehicle_journeys.form.departure_at")}> <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} + disabled={!this.props.editMode || 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']} @@ -123,13 +149,16 @@ 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} + disabled={!this.props.editMode || 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']} /> </span> </div> + {vj.errors && <div className="errors"> + {vj.errors} + </div>} </div> </div> )} @@ -143,5 +172,6 @@ VehicleJourney.propTypes = { filters: PropTypes.object.isRequired, index: PropTypes.number.isRequired, onUpdateTime: PropTypes.func.isRequired, - onSelectVehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file + onSelectVehicleJourney: PropTypes.func.isRequired, + vehicleJourneys: PropTypes.object.isRequired, +} diff --git a/app/javascript/vehicle_journeys/components/VehicleJourneys.js b/app/javascript/vehicle_journeys/components/VehicleJourneys.js index 6bce9766b..01e07ee0c 100644 --- a/app/javascript/vehicle_journeys/components/VehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/VehicleJourneys.js @@ -1,15 +1,51 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import _ from 'lodash' import VehicleJourney from './VehicleJourney' - +import StopAreaHeaderManager from '../../helpers/stop_area_header_manager' export default class VehicleJourneys extends Component { constructor(props){ super(props) - this.previousCity = undefined + this.headerManager = new StopAreaHeaderManager( + _.map(this.stopPoints(), (sp)=>{return sp.object_id}), + this.stopPoints(), + this.props.filters.features + ) + } + + isReturn() { + return this.props.routeUrl != undefined + } + + vehicleJourneysList() { + if(this.isReturn()){ + return this.props.returnVehicleJourneys + } + else{ + return this.props.vehicleJourneys + } + } + + stopPoints() { + if(this.isReturn()){ + return this.props.returnStopPointsList + } + else{ + return this.props.stopPointsList + } } + componentDidMount() { - this.props.onLoadFirstPage(this.props.filters) + this.props.onLoadFirstPage(this.props.filters, this.props.routeUrl) + } + + hasFeature(key) { + return this.props.filters.features[key] + } + + showHeader(object_id) { + return this.headerManager.showHeader(object_id) } componentDidUpdate(prevProps, prevState) { @@ -55,25 +91,8 @@ export default class VehicleJourneys extends Component { } } - cityNameChecker(sp) { - let bool = false - if(sp.city_name != this.previousCity){ - bool = true - this.previousCity = sp.city_name - } - return ( - <div - className={(bool) ? 'headlined' : ''} - data-headline={(bool) ? sp.city_name : ''} - title={sp.city_name + ' (' + sp.zip_code +')'} - > - <span><span>{sp.name}</span></span> - </div> - ) - } - render() { - this.previousCity = undefined + this.previousBreakpoint = undefined if(this.props.status.isFetching == true) { return ( @@ -87,15 +106,15 @@ export default class VehicleJourneys extends Component { <div className='col-lg-12'> {(this.props.status.fetchSuccess == false) && ( <div className='alert alert-danger mt-sm'> - <strong>Erreur : </strong> - la récupération des missions a rencontré un problème. Rechargez la page pour tenter de corriger le problème. + <strong>{I18n.tc("error")}</strong> + {I18n.t("vehicle_journeys.vehicle_journeys_matrix.fetching_error")} </div> )} - { _.some(this.props.vehicleJourneys, 'errors') && ( + { this.vehicleJourneysList().errors && this.vehicleJourneysList().errors.length && _.some(this.vehicleJourneysList(), 'errors') && ( <div className="alert alert-danger mt-sm"> - <strong>Erreur : </strong> - {this.props.vehicleJourneys.map((vj, index) => + <strong>{I18n.tc("error")}</strong> + {this.vehicleJourneysList().map((vj, index) => vj.errors && vj.errors.map((err, i) => { return ( <ul key={i}> @@ -107,17 +126,20 @@ export default class VehicleJourneys extends Component { </div> )} - <div className={'table table-2entries mt-sm mb-sm' + ((this.props.vehicleJourneys.length > 0) ? '' : ' no_result')}> + <div className={'table table-2entries mt-sm mb-sm' + ((this.vehicleJourneysList().length > 0) ? '' : ' no_result')}> <div className='t2e-head w20'> <div className='th'> - <div className='strong mb-xs'>ID course</div> - <div>ID mission</div> - <div>Calendriers</div> + <div className='strong mb-xs'>{I18n.attribute_name("vehicle_journey", "id")}</div> + <div>{I18n.attribute_name("vehicle_journey", "name")}</div> + <div>{I18n.attribute_name("vehicle_journey", "journey_pattern_id")}</div> + <div>{I18n.model_name("company")}</div> + <div>{I18n.model_name("time_table", "plural": true)}</div> + { this.hasFeature('purchase_windows') && <div>{I18n.model_name("purchase_window", "plural": true)}</div> } </div> - {this.props.stopPointsList.map((sp, i) =>{ + {this.stopPoints().map((sp, i) =>{ return ( <div key={i} className='td'> - {this.cityNameChecker(sp)} + {this.headerManager.stopPointHeader(sp.object_id)} </div> ) })} @@ -125,15 +147,18 @@ export default class VehicleJourneys extends Component { <div className='t2e-item-list w80'> <div> - {this.props.vehicleJourneys.map((vj, index) => + {this.vehicleJourneysList().map((vj, index) => <VehicleJourney value={vj} key={index} index={index} - editMode={this.props.editMode} + editMode={this.isReturn() ? false : this.props.editMode} filters={this.props.filters} + features={this.props.features} onUpdateTime={this.props.onUpdateTime} onSelectVehicleJourney={this.props.onSelectVehicleJourney} + vehicleJourneys={this} + disabled={this.isReturn()} /> )} </div> @@ -153,4 +178,4 @@ VehicleJourneys.propTypes = { onLoadFirstPage: PropTypes.func.isRequired, onUpdateTime: PropTypes.func.isRequired, onSelectVehicleJourney: PropTypes.func.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/CreateModal.js b/app/javascript/vehicle_journeys/components/tools/CreateModal.js index 33873219c..8536f66e6 100644 --- a/app/javascript/vehicle_journeys/components/tools/CreateModal.js +++ b/app/javascript/vehicle_journeys/components/tools/CreateModal.js @@ -1,16 +1,19 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' import MissionSelect2 from './select2s/MissionSelect2' import CompanySelect2 from './select2s/CompanySelect2' +import CustomFieldsInputs from './CustomFieldsInputs' export default class CreateModal extends Component { constructor(props) { super(props) + this.custom_fields = _.assign({}, this.props.custom_fields) } handleSubmit() { if (actions.validateFields(...this.refs, $('.vjCreateSelectJP')[0]) && this.props.modal.modalProps.selectedJPModal) { - this.props.onAddVehicleJourney(this.refs, this.props.modal.modalProps.selectedJPModal, this.props.stopPointsList, this.props.modal.modalProps.selectedCompany) + this.props.onAddVehicleJourney(_.assign({}, this.refs, {custom_fields: this.custom_fields}), this.props.modal.modalProps.selectedJPModal, this.props.stopPointsList, this.props.modal.modalProps.vehicleJourney && this.props.modal.modalProps.vehicleJourney.company) this.props.onModalClose() $('#NewVehicleJourneyModal').modal('hide') } @@ -61,8 +64,9 @@ export default class CreateModal extends Component { <div className='form-group'> <label className='control-label'>Nom du transporteur</label> <CompanySelect2 - company = {undefined} + company = {this.props.modal.modalProps.vehicleJourney && this.props.modal.modalProps.vehicleJourney.company || undefined} onSelect2Company = {(e) => this.props.onSelect2Company(e)} + onUnselect2Company = {() => this.props.onUnselect2Company()} /> </div> </div> @@ -72,6 +76,7 @@ export default class CreateModal extends Component { <MissionSelect2 selection={this.props.modal.modalProps} onSelect2JourneyPattern={this.props.onSelect2JourneyPattern} + values={this.props.missions} isFilter={false} /> </div> @@ -87,6 +92,36 @@ export default class CreateModal extends Component { /> </div> </div> + <CustomFieldsInputs + values={this.props.custom_fields} + onUpdate={(code, value) => this.custom_fields[code]["value"] = value} + disabled={false} + /> + { this.props.modal.modalProps.selectedJPModal && this.props.modal.modalProps.selectedJPModal.full_schedule && <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> + <div className='form-group'> + <label className='control-label'>Heure de départ</label> + <div className='input-group time'> + <input + type='number' + min='00' + max='23' + ref='start_time.hour' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + /> + <input + type='number' + min='00' + max='59' + ref='start_time.minute' + className='form-control' + onKeyDown={(e) => actions.resetValidation(e.currentTarget)} + /> + </div> + </div> + </div> + } + </div> </div> <div className='modal-footer'> @@ -129,5 +164,6 @@ CreateModal.propTypes = { onModalClose: PropTypes.func.isRequired, onAddVehicleJourney: PropTypes.func.isRequired, onSelect2JourneyPattern: PropTypes.func.isRequired, - disabled: PropTypes.bool.isRequired -}
\ No newline at end of file + disabled: PropTypes.bool.isRequired, + missions: PropTypes.array.isRequired +} diff --git a/app/javascript/vehicle_journeys/components/tools/CustomFieldsInputs.js b/app/javascript/vehicle_journeys/components/tools/CustomFieldsInputs.js new file mode 100644 index 000000000..90d72a801 --- /dev/null +++ b/app/javascript/vehicle_journeys/components/tools/CustomFieldsInputs.js @@ -0,0 +1,50 @@ +import _ from 'lodash' +import Select2 from 'react-select2-wrapper' +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +export default class CustomFieldsInputs extends Component { + constructor(props) { + super(props) + } + + listInput(cf){ + return( + <Select2 + data={_.map(cf.options.list_values, (v, k) => { + return {id: k, text: (v.length > 0 ? v : '\u00A0')} + })} + ref={'custom_fields.' + cf.code} + className='form-control' + defaultValue={cf.value} + disabled={this.props.disabled} + options={{ + theme: 'bootstrap', + width: '100%' + }} + onSelect={(e) => this.props.onUpdate(cf.code, e.params.data.id) } + /> + ) + } + + render() { + return ( + <div> + {_.map(this.props.values, (cf, code) => + <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12' key={code}> + <div className='form-group'> + <label className='control-label'>{cf.name}</label> + {this[cf.field_type + "Input"](cf)} + </div> + </div> + )} + </div> + ) + } +} + +CustomFieldsInputs.propTypes = { + onUpdate: PropTypes.func.isRequired, + values: PropTypes.object.isRequired, + disabled: PropTypes.bool.isRequired +} diff --git a/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js index fc13ae964..4815003d3 100644 --- a/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js +++ b/app/javascript/vehicle_journeys/components/tools/DeleteVehicleJourneys.js @@ -1,4 +1,6 @@ -import React, { PropTypes } from 'react' +import React from 'react' +import PropTypes from 'prop-types' + import actions from '../../actions' export default function DeleteVehicleJourneys({onDeleteVehicleJourneys, vehicleJourneys, disabled}) { diff --git a/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js index 8083defb9..102a87d85 100644 --- a/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/DuplicateVehicleJourney.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' import _ from 'lodash' diff --git a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js index f8d6add03..d3c01f154 100644 --- a/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/EditVehicleJourney.js @@ -1,33 +1,47 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' import CompanySelect2 from './select2s/CompanySelect2' +import CustomFieldsInputs from './CustomFieldsInputs' export default class EditVehicleJourney extends Component { constructor(props) { super(props) + this.updateValue = this.updateValue.bind(this) } handleSubmit() { if(actions.validateFields(this.refs) == true) { - var company; + var company = undefined if(this.props.modal.modalProps.selectedCompany) { company = this.props.modal.modalProps.selectedCompany - } else if (typeof this.props.modal.modalProps.vehicleJourney.company === Object) { + } else if (typeof this.props.modal.modalProps.vehicleJourney.company === "object") { company = this.props.modal.modalProps.vehicleJourney.company - } else { - company = undefined } - this.props.onEditVehicleJourney(this.refs, company) + this.props.onEditVehicleJourney(_.assign({}, this.refs, {custom_fields: this.custom_fields}), company) this.props.onModalClose() $('#EditVehicleJourneyModal').modal('hide') } } + updateValue(attribute, e) { + this.props.modal.modalProps.vehicleJourney[attribute] = e.target.value + actions.resetValidation(e.currentTarget) + this.forceUpdate() + } + + editMode() { + return !this.props.modal.modalProps.info && this.props.editMode + } + render() { if(this.props.status.isFetching == true) { return false } if(this.props.status.fetchSuccess == true) { + if(this.props.modal.modalProps.vehicleJourney){ + this.custom_fields = _.assign({}, this.props.modal.modalProps.vehicleJourney.custom_fields) + } return ( <li className='st_action'> <button @@ -45,60 +59,60 @@ export default class EditVehicleJourney extends Component { <div className='modal-dialog'> <div className='modal-content'> <div className='modal-header'> - <h4 className='modal-title'>Informations</h4> + <h4 className='modal-title'>{I18n.t('vehicle_journeys.form.infos')}</h4> <span type="button" className="close modal-close" data-dismiss="modal">×</span> </div> {(this.props.modal.type == 'edit') && ( <form> <div className='modal-body'> - <div className='row'> - <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> - <div className='form-group'> - <label className='control-label'>Nom de la course</label> - <input - 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)} - /> + <div className='row'> + <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> + <div className='form-group'> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'journey_name')}</label> + <input + type='text' + ref='published_journey_name' + className='form-control' + disabled={!this.editMode()} + value={this.props.modal.modalProps.vehicleJourney.published_journey_name} + onChange={(e) => this.updateValue('published_journey_name', e)} + /> + </div> </div> - </div> - <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> - <div className='form-group'> - <label className='control-label'>Mission</label> - <input - type='text' - className='form-control' - value={this.props.modal.modalProps.vehicleJourney.journey_pattern.short_id + ' - ' + (this.props.modal.modalProps.vehicleJourney.journey_pattern.name)} - disabled={true} - /> + <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> + <div className='form-group'> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'journey_pattern')}</label> + <input + type='text' + className='form-control' + value={this.props.modal.modalProps.vehicleJourney.journey_pattern.short_id + ' - ' + (this.props.modal.modalProps.vehicleJourney.journey_pattern.name)} + disabled={true} + /> + </div> </div> </div> - </div> <div className='row'> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> <div className='form-group'> - <label className='control-label'>Numéro de train</label> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'company')}</label> <input 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)} + disabled={!this.editMode()} + value={this.props.modal.modalProps.vehicleJourney.published_journey_identifier} + onChange={(e) => this.updateValue('published_journey_identifier', e)} /> </div> </div> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> <div className='form-group'> - <label className='control-label'>Transporteur</label> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'company')}</label> <CompanySelect2 editModal={this.props.modal.type == "edit"} - editMode={this.props.editMode} + editMode={this.editMode()} company = {this.props.modal.modalProps.vehicleJourney.company} onSelect2Company = {(e) => this.props.onSelect2Company(e)} onUnselect2Company = {() => this.props.onUnselect2Company()} @@ -110,30 +124,48 @@ export default class EditVehicleJourney extends Component { <div className='row'> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> <div className='form-group'> - <label className='control-label'>Mode de transport</label> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'transport_mode')}</label> <input type='text' className='form-control' - value={window.I18n.fr.enumerize.transport_mode[this.props.modal.modalProps.vehicleJourney.transport_mode]} + value={I18n.enumerize('transport_mode', this.props.modal.modalProps.vehicleJourney.transport_mode)} disabled={true} /> </div> </div> <div className='col-lg-6 col-md-6 col-sm-6 col-xs-12'> <div className='form-group'> - <label className='control-label'>Sous mode de transport</label> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'transport_submode')}</label> <input type='text' className='form-control' - value={window.I18n.fr.enumerize.transport_submode[this.props.modal.modalProps.vehicleJourney.transport_submode]} + value={I18n.enumerize('transport_submode', this.props.modal.modalProps.vehicleJourney.transport_submode)} disabled={true} /> </div> </div> </div> + <div className='form-group'> + <label className='control-label'>{I18n.attribute_name('vehicle_journey', 'checksum')}</label> + <input + type='text' + ref='checksum' + className='form-control' + disabled='disabled' + defaultValue={this.props.modal.modalProps.vehicleJourney.checksum} + /> + </div> + <div className='row'> + <CustomFieldsInputs + values={this.props.modal.modalProps.vehicleJourney.custom_fields} + onUpdate={(code, value) => this.custom_fields[code]["value"] = value} + disabled={!this.editMode()} + /> + </div> </div> + { - this.props.editMode && + this.editMode() && <div className='modal-footer'> <button className='btn btn-link' @@ -151,7 +183,7 @@ export default class EditVehicleJourney extends Component { Valider </button> </div> - } + } </form> )} @@ -171,4 +203,4 @@ EditVehicleJourney.propTypes = { onOpenEditModal: PropTypes.func.isRequired, onModalClose: PropTypes.func.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 de97bc403..880542216 100644 --- a/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/NotesEditVehicleJourney.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' import _ from 'lodash' diff --git a/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js new file mode 100644 index 000000000..ce9a4cde9 --- /dev/null +++ b/app/javascript/vehicle_journeys/components/tools/PurchaseWindowsEditVehicleJourney.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import actions from '../../actions' +import TimetableSelect2 from './select2s/TimetableSelect2' + +export default class PurchaseWindowsEditVehicleJourney extends Component { + constructor(props) { + super(props) + this.handleSubmit = this.handleSubmit.bind(this) + this.purchaseWindowURL = this.purchaseWindowURL.bind(this) + } + + handleSubmit() { + this.props.onShoppingWindowsEditVehicleJourney(this.props.modal.modalProps.vehicleJourneys, this.props.modal.modalProps.purchase_windows) + this.props.onModalClose() + $('#PurchaseWindowsEditVehicleJourneyModal').modal('hide') + } + + purchaseWindowURL(tt) { + let refURL = window.location.pathname.split('/', 3).join('/') + return refURL + '/purchase_windows/' + tt.id + } + + render() { + 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.disabled)} + data-toggle='modal' + data-target='#PurchaseWindowsEditVehicleJourneyModal' + onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))} + title='Calendriers commerciaux' + > + <span className='sb sb-purchase_window sb-strong'></span> + </button> + + <div className={ 'modal fade ' + ((this.props.modal.type == 'duplicate') ? 'in' : '') } id='PurchaseWindowsEditVehicleJourneyModal'> + <div className='modal-container'> + <div className='modal-dialog'> + <div className='modal-content'> + <div className='modal-header'> + <h4 className='modal-title'>Calendriers commerciaux associés</h4> + <span type="button" className="close modal-close" data-dismiss="modal">×</span> + </div> + + {(this.props.modal.type == 'purchase_windows_edit') && ( + <form> + <div className='modal-body'> + <div className='row'> + <div className='col-lg-12'> + <div className='subform'> + <div className='nested-head'> + <div className='wrapper'> + <div> + <div className='form-group'> + <label className='control-label'>{this.props.modal.modalProps.purchase_windows.length == 0 ? "Aucun calendrier commercial associé" : "Calendriers commerciaux associés"}</label> + </div> + </div> + <div></div> + </div> + </div> + {this.props.modal.modalProps.purchase_windows.map((tt, i) => + <div className='nested-fields' key={i}> + <div className='wrapper'> + <div> <a href={this.purchaseWindowURL(tt)} target="_blank"> + <span className="fa fa-circle mr-xs" style={{color: tt.color}}></span> + {tt.name} + </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> + )} + { + this.props.editMode && + <div className='nested-fields'> + <div className='wrapper'> + <div> + <TimetableSelect2 + onSelect2Timetable={this.props.onSelect2Timetable} + chunkURL={'/autocomplete_purchase_windows.json'} + searchKey={"name_or_objectid_cont_any"} + isFilter={false} + /> + </div> + </div> + </div> + } + </div> + </div> + </div> + </div> + { + 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} + > + Valider + </button> + </div> + } + </form> + )} + + </div> + </div> + </div> + </div> + </li> + ) + } else { + return false + } + } +} + +PurchaseWindowsEditVehicleJourney.propTypes = { + onOpenCalendarsEditModal: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onShoppingWindowsEditVehicleJourney: PropTypes.func.isRequired, + onDeleteCalendarModal: PropTypes.func.isRequired, + onSelect2Timetable: PropTypes.func.isRequired, + disabled: PropTypes.bool.isRequired +} diff --git a/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js index a54e40502..6574bfa2d 100644 --- a/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/ShiftVehicleJourney.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' export default class ShiftVehicleJourney extends Component { diff --git a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js index 6629135dd..e2fcd27d5 100644 --- a/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js +++ b/app/javascript/vehicle_journeys/components/tools/TimetablesEditVehicleJourney.js @@ -1,4 +1,5 @@ -import React, { PropTypes, Component } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import actions from '../../actions' import TimetableSelect2 from './select2s/TimetableSelect2' @@ -33,6 +34,7 @@ export default class TimetablesEditVehicleJourney extends Component { data-toggle='modal' data-target='#CalendarsEditVehicleJourneyModal' onClick={() => this.props.onOpenCalendarsEditModal(actions.getSelected(this.props.vehicleJourneys))} + title='Calendriers' > <span className='fa fa-calendar'></span> </button> @@ -65,9 +67,14 @@ export default class TimetablesEditVehicleJourney extends Component { {this.props.modal.modalProps.timetables.map((tt, i) => <div className='nested-fields' key={i}> <div className='wrapper'> - <div> <a href={this.timeTableURL(tt)} target="_blank">{tt.comment}</a> </div> + <div> + <a href={this.timeTableURL(tt)} target="_blank"> + <span className="fa fa-circle mr-xs" style={{color: tt.color || 'black'}}></span> + {tt.comment} + </a> + </div> { - this.props.editMode && + this.props.editMode && <div> <a href='#' @@ -85,13 +92,14 @@ export default class TimetablesEditVehicleJourney extends Component { </div> )} { - this.props.editMode && + this.props.editMode && <div className='nested-fields'> <div className='wrapper'> <div> <TimetableSelect2 onSelect2Timetable={this.props.onSelect2Timetable} chunkURL={'/autocomplete_time_tables.json'} + searchKey={"comment_or_objectid_cont_any"} isFilter={false} /> </div> @@ -103,7 +111,7 @@ export default class TimetablesEditVehicleJourney extends Component { </div> </div> { - this.props.editMode && + this.props.editMode && <div className='modal-footer'> <button className='btn btn-link' @@ -144,4 +152,4 @@ TimetablesEditVehicleJourney.propTypes = { onDeleteCalendarModal: PropTypes.func.isRequired, onSelect2Timetable: PropTypes.func.isRequired, disabled: PropTypes.bool.isRequired -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/VehicleJourneyInfoButton.js b/app/javascript/vehicle_journeys/components/tools/VehicleJourneyInfoButton.js new file mode 100644 index 000000000..538bbdbd6 --- /dev/null +++ b/app/javascript/vehicle_journeys/components/tools/VehicleJourneyInfoButton.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import actions from '../../actions' + +export default class VehicleJourneyInfoButton extends Component { + constructor(props) { + super(props) + } + + + render() { + return ( + <div className='info-button'> + <button + type='button' + data-toggle='modal' + data-target='#EditVehicleJourneyModal' + onClick={() => this.props.onOpenEditModal(this.props.vehicleJourney)} + > + <span className='fa fa-info'></span> + </button> + </div> + ) + } +} + +VehicleJourneyInfoButton.propTypes = { + onOpenEditModal: PropTypes.func.isRequired, + vehicleJourney: PropTypes.object.isRequired, +} diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/CompanySelect2.js index 79ba8f094..5c7f75d99 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 React, { Component } from 'react' +import PropTypes from 'prop-types' +import Select2 from 'react-select2-wrapper' import actions from '../../../actions' // get JSON full path @@ -25,7 +26,7 @@ export default class BSelect4 extends Component { multiple={false} ref='company_id' options={{ - allowClear: this.props.editMode, + allowClear: true, theme: 'bootstrap', width: '100%', placeholder: 'Filtrer par transporteur...', diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js index fa847886c..7ab85a1ea 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/MissionSelect2.js @@ -1,6 +1,7 @@ import _ from 'lodash' -import React, { PropTypes, Component } from 'react' -import Select2 from 'react-select2' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Select2 from 'react-select2-wrapper' import actions from '../../../actions' // get JSON full path @@ -11,50 +12,114 @@ let path = window.location.pathname.split('/', 7).join('/') export default class BSelect4 extends Component { constructor(props) { super(props) + this.onSelect = this.onSelect.bind(this) + } + + useAjax(){ + return this.props.values == undefined || this.props.values.length == 0 + } + + value(){ + let val = undefined + if(this.props.isFilter) { + val = this.props.filters.query.journeyPattern + } + else{ + if(this.props.selection.selectedJPModal){ + val = this.props.selection.selectedJPModal + } + } + if(this.useAjax()){ + val = val.published_name + } + else{ + if(val){ + val = val.id + } + } + return val + } + + data(){ + if(!this.useAjax()){ + let values = [{}] + values.push(...this.props.values) + return values + } + if(this.props.isFilter){ + return [this.props.filters.query.journeyPattern.published_name] + } + + return (this.props.selection.selectedJPModal) ? [this.props.selection.selectedJPModal.published_name] : undefined + } + + onSelect(e){ + if(this.useAjax()){ + this.props.onSelect2JourneyPattern(e) + } + else{ + let data = JSON.parse(e.currentTarget.selectedOptions[0].dataset.item) + + this.props.onSelect2JourneyPattern({params: + { + data: _.assign({}, e.params.data, data) + } + }) + } + } + + options(){ + let options = { + theme: 'bootstrap', + width: '100%', + escapeMarkup: function (markup) { return markup; }, + templateResult: formatRepo, + placeholder: 'Filtrer par code, nom ou OID de mission...', + language: require('./fr'), + allowClear: false, + escapeMarkup: function (markup) { return markup; }, + } + if(this.useAjax()){ + options = _.assign({}, options, { + ajax: { + url: origin + path + '/journey_patterns_collection.json', + dataType: 'json', + delay: '500', + data: function(params) { + return { + q: { published_name_or_objectid_or_registration_number_cont: params.term}, + }; + }, + processResults: function(data, params) { + return { + results: data.map( + item => _.assign( + {}, + item, + { text: "<strong>" + item.published_name + " - " + item.short_id + "</strong><br/><small>" + item.registration_number + "</small>" } + ) + ) + }; + }, + cache: true + }, + minimumInputLength: 1 + }) + } + return options } render() { return ( <Select2 - data={(this.props.isFilter) ? [this.props.filters.query.journeyPattern.published_name] : ((this.props.selection.selectedJPModal) ? [this.props.selection.selectedJPModal.published_name] : undefined)} - value={(this.props.isFilter) ? this.props.filters.query.journeyPattern.published_name : ((this.props.selection.selectedJPModal) ? this.props.selection.selectedJPModal.published_name : undefined) } - onSelect={(e) => this.props.onSelect2JourneyPattern(e)} + data={this.data()} + value={this.value()} + onSelect={this.onSelect} multiple={false} ref='journey_pattern_id' className={!this.props.isFilter ? "vjCreateSelectJP" : null} required={!this.props.isFilter} - options={{ - allowClear: false, - theme: 'bootstrap', - placeholder: 'Filtrer par code, nom ou OID de mission...', - language: require('./fr'), - width: '100%', - ajax: { - url: origin + path + '/journey_patterns_collection.json', - dataType: 'json', - delay: '500', - data: function(params) { - return { - q: { published_name_or_objectid_or_registration_number_cont: params.term}, - }; - }, - processResults: function(data, params) { - return { - results: data.map( - item => _.assign( - {}, - item, - { text: "<strong>" + item.published_name + " - " + item.short_id + "</strong><br/><small>" + item.registration_number + "</small>" } - ) - ) - }; - }, - cache: true - }, - minimumInputLength: 1, - escapeMarkup: function (markup) { return markup; }, - templateResult: formatRepo - }} + options={this.options()} /> ) } @@ -62,4 +127,4 @@ export default class BSelect4 extends Component { const formatRepo = (props) => { if(props.text) return props.text -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js index 19c183839..0339455ca 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/TimetableSelect2.js @@ -1,6 +1,7 @@ import _ from 'lodash' -import React, { PropTypes, Component } from 'react' -import Select2 from 'react-select2' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Select2 from 'react-select2-wrapper' import actions from '../../../actions' // get JSON full path @@ -31,12 +32,10 @@ export default class BSelect4 extends Component { url: origin + path + this.props.chunkURL, dataType: 'json', delay: '500', - data: function(params) { - return { - q: { - comment_or_objectid_cont_any: params.term - } - }; + data: (params) => { + let q = {} + q[this.props.searchKey] = params.term + return {q} }, processResults: function(data, params) { return { @@ -44,7 +43,7 @@ export default class BSelect4 extends Component { item => _.assign( {}, item, - {text: '<strong>' + "<span class='fa fa-circle' style='color:" + (item.color ? item.color : '#4B4B4B') + "'></span> " + item.comment + ' - ' + item.short_id + '</strong><br/><small>' + (item.day_types ? item.day_types.match(/[A-Z]?[a-z]+/g).join(', ') : "") + '</small>'} + {text: '<strong>' + "<span class='fa fa-circle' style='color:" + (item.color ? item.color : '#4B4B4B') + "'></span> " + (item.comment || item.name) + ' - ' + item.short_id + '</strong><br/><small>' + (item.day_types ? item.day_types.match(/[A-Z]?[a-z]+/g).join(', ') : "") + '</small>'} ) ) }; @@ -62,4 +61,4 @@ export default class BSelect4 extends Component { const formatRepo = (props) => { if(props.text) return props.text -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js index b063abeca..ccb4c9595 100644 --- a/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js +++ b/app/javascript/vehicle_journeys/components/tools/select2s/VJSelect2.js @@ -1,6 +1,7 @@ import _ from 'lodash' -import React, { PropTypes, Component } from 'react' -import Select2 from 'react-select2' +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Select2 from 'react-select2-wrapper' import actions from '../../../actions' // get JSON full path diff --git a/app/javascript/vehicle_journeys/containers/Filters.js b/app/javascript/vehicle_journeys/containers/Filters.js index bec3527f4..a41c599f7 100644 --- a/app/javascript/vehicle_journeys/containers/Filters.js +++ b/app/javascript/vehicle_journeys/containers/Filters.js @@ -5,7 +5,8 @@ import Filters from '../components/Filters' const mapStateToProps = (state) => { return { filters: state.filters, - pagination: state.pagination + pagination: state.pagination, + missions: state.missions, } } diff --git a/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js index 18f9e994e..3daf831f8 100644 --- a/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js +++ b/app/javascript/vehicle_journeys/containers/SaveVehicleJourneys.js @@ -17,8 +17,15 @@ const mapDispatchToProps = (dispatch) => { onEnterEditMode: () => { dispatch(actions.enterEditMode()) }, + onExitEditMode: () => { + dispatch(actions.cancelSelection()) + dispatch(actions.exitEditMode()) + }, onSubmitVehicleJourneys: (next, state) => { actions.submitVehicleJourneys(dispatch, state, next) + }, + validate: (state) =>{ + actions.validate(dispatch, state) } } } diff --git a/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js b/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js index 38ab9f6d3..76d1c3a78 100644 --- a/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js +++ b/app/javascript/vehicle_journeys/containers/VehicleJourneysList.js @@ -6,17 +6,19 @@ const mapStateToProps = (state) => { return { editMode: state.editMode, vehicleJourneys: state.vehicleJourneys, + returnVehicleJourneys: state.returnVehicleJourneys, status: state.status, filters: state.filters, - stopPointsList: state.stopPointsList + stopPointsList: state.stopPointsList, + returnStopPointsList: state.returnStopPointsList } } const mapDispatchToProps = (dispatch) => { return { - onLoadFirstPage: (filters) =>{ + onLoadFirstPage: (filters, routeUrl) =>{ dispatch(actions.fetchingApi()) - actions.fetchVehicleJourneys(dispatch, undefined, undefined, filters.queryString) + actions.fetchVehicleJourneys(dispatch, undefined, undefined, filters.queryString, routeUrl) }, onUpdateTime: (e, subIndex, index, timeUnit, isDeparture, isArrivalsToggled) => { dispatch(actions.updateTime(e.target.value, subIndex, index, timeUnit, isDeparture, isArrivalsToggled)) diff --git a/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js index 5da0bd3e9..d982f5a5f 100644 --- a/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js +++ b/app/javascript/vehicle_journeys/containers/tools/AddVehicleJourney.js @@ -9,6 +9,8 @@ const mapStateToProps = (state, ownProps) => { vehicleJourneys: state.vehicleJourneys, status: state.status, stopPointsList: state.stopPointsList, + missions: state.missions, + custom_fields: state.custom_fields, } } @@ -28,6 +30,9 @@ const mapDispatchToProps = (dispatch) => { }, onSelect2Company: (e) => { dispatch(actions.select2Company(e.params.data)) + }, + onUnselect2Company: () => { + dispatch(actions.unselect2Company()) } } } diff --git a/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js b/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js new file mode 100644 index 000000000..3fef44489 --- /dev/null +++ b/app/javascript/vehicle_journeys/containers/tools/PurchaseWindowsEditVehicleJourney.js @@ -0,0 +1,38 @@ +import actions from '../../actions' +import { connect } from 'react-redux' +import PurchaseWindowsEditVehicleJourneyComponent from '../../components/tools/PurchaseWindowsEditVehicleJourney' + +const mapStateToProps = (state, ownProps) => { + return { + editMode: state.editMode, + modal: state.modal, + vehicleJourneys: state.vehicleJourneys, + status: state.status, + disabled: ownProps.disabled + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onModalClose: () =>{ + dispatch(actions.closeModal()) + }, + onOpenCalendarsEditModal: (vehicleJourneys) =>{ + dispatch(actions.openPurchaseWindowsEditModal(vehicleJourneys)) + }, + onDeleteCalendarModal: (timetable) => { + dispatch(actions.deletePurchaseWindowsModal(timetable)) + }, + onShoppingWindowsEditVehicleJourney: (vehicleJourneys, timetables) =>{ + dispatch(actions.editVehicleJourneyPurchaseWindows(vehicleJourneys, timetables)) + }, + onSelect2Timetable: (e) =>{ + dispatch(actions.selectPurchaseWindowsModal(e.params.data)) + dispatch(actions.addSelectedPurchaseWindow()) + } + } +} + +const PurchaseWindowsEditVehicleJourney = connect(mapStateToProps, mapDispatchToProps)(PurchaseWindowsEditVehicleJourneyComponent) + +export default PurchaseWindowsEditVehicleJourney diff --git a/app/javascript/vehicle_journeys/containers/tools/VehicleJourneyInfoButton.js b/app/javascript/vehicle_journeys/containers/tools/VehicleJourneyInfoButton.js new file mode 100644 index 000000000..19010c312 --- /dev/null +++ b/app/javascript/vehicle_journeys/containers/tools/VehicleJourneyInfoButton.js @@ -0,0 +1,20 @@ +import actions from '../../actions' +import { connect } from 'react-redux' +import VehicleJourneyInfoButtonComponent from '../../components/tools/VehicleJourneyInfoButton' + +const mapStateToProps = (state, ownProps) => { + return { + } +} + +const mapDispatchToProps = (dispatch) => { + return { + onOpenEditModal: (vj) =>{ + dispatch(actions.openInfoModal(vj)) + }, + } +} + +const VehicleJourneyInfoButton = connect(mapStateToProps, mapDispatchToProps)(VehicleJourneyInfoButtonComponent) + +export default VehicleJourneyInfoButton diff --git a/app/javascript/vehicle_journeys/reducers/custom_fields.js b/app/javascript/vehicle_journeys/reducers/custom_fields.js new file mode 100644 index 000000000..482fd91cb --- /dev/null +++ b/app/javascript/vehicle_journeys/reducers/custom_fields.js @@ -0,0 +1,6 @@ +export default function custom_fields(state = [], action) { + switch (action.type) { + default: + return state + } +} diff --git a/app/javascript/vehicle_journeys/reducers/index.js b/app/javascript/vehicle_journeys/reducers/index.js index bb24aa185..95ac9c7e1 100644 --- a/app/javascript/vehicle_journeys/reducers/index.js +++ b/app/javascript/vehicle_journeys/reducers/index.js @@ -1,20 +1,27 @@ import { combineReducers } from 'redux' import vehicleJourneys from './vehicleJourneys' +import returnVehicleJourneys from './returnVehicleJourneys' import pagination from './pagination' import modal from './modal' import status from './status' import filters from './filters' import editMode from './editMode' import stopPointsList from './stopPointsList' +import missions from './missions' +import custom_fields from './custom_fields' const vehicleJourneysApp = combineReducers({ vehicleJourneys, + returnVehicleJourneys, pagination, modal, status, filters, editMode, - stopPointsList + stopPointsList, + returnStopPointsList: stopPointsList, + missions, + custom_fields }) export default vehicleJourneysApp diff --git a/app/javascript/vehicle_journeys/reducers/missions.js b/app/javascript/vehicle_journeys/reducers/missions.js new file mode 100644 index 000000000..7c1a355c7 --- /dev/null +++ b/app/javascript/vehicle_journeys/reducers/missions.js @@ -0,0 +1,6 @@ +export default function missions(state = [], action) { + switch (action.type) { + default: + return state + } +} diff --git a/app/javascript/vehicle_journeys/reducers/modal.js b/app/javascript/vehicle_journeys/reducers/modal.js index 57f54a144..bcfc6ea0b 100644 --- a/app/javascript/vehicle_journeys/reducers/modal.js +++ b/app/javascript/vehicle_journeys/reducers/modal.js @@ -1,6 +1,6 @@ import _ from 'lodash' -let vehicleJourneysModal, newModalProps +let vehicleJourneysModal, newModalProps, vehicleJourney export default function modal(state = {}, action) { switch (action.type) { @@ -37,10 +37,18 @@ export default function modal(state = {}, action) { }, confirmModal: {} } + case 'INFO_VEHICLEJOURNEY_MODAL': + return { + type: 'edit', + modalProps: { + vehicleJourney: action.vehicleJourney, + info: true + }, + confirmModal: {} + } case 'EDIT_CALENDARS_VEHICLEJOURNEY_MODAL': vehicleJourneysModal = JSON.parse(JSON.stringify(action.vehicleJourneys)) let uniqTimetables = [] - let timetable = {} vehicleJourneysModal.map((vj, i) => { vj.time_tables.map((tt, j) =>{ if(!(_.find(uniqTimetables, tt))){ @@ -56,15 +64,38 @@ export default function modal(state = {}, action) { }, confirmModal: {} } + case 'EDIT_PURCHASE_WINDOWS_VEHICLEJOURNEY_MODAL': + var vehicleJourneys = JSON.parse(JSON.stringify(action.vehicleJourneys)) + let uniqPurchaseWindows = [] + vehicleJourneys.map((vj, i) => { + vj.purchase_windows.map((pw, j) =>{ + if(!(_.find(uniqPurchaseWindows, pw))){ + uniqPurchaseWindows.push(pw) + } + }) + }) + return { + type: 'purchase_windows_edit', + modalProps: { + vehicleJourneys: vehicleJourneys, + purchase_windows: uniqPurchaseWindows + }, + confirmModal: {} + } case 'SELECT_CP_EDIT_MODAL': - newModalProps = _.assign({}, state.modalProps, {selectedCompany : action.selectedItem}) + vehicleJourney = _.assign({}, state.modalProps.vehicleJourney, {company: action.selectedItem}) + newModalProps = _.assign({}, state.modalProps, {vehicleJourney}) return _.assign({}, state, {modalProps: newModalProps}) case 'UNSELECT_CP_EDIT_MODAL': - newModalProps = _.assign({}, state.modalProps, {selectedCompany : undefined}) + vehicleJourney = _.assign({}, state.modalProps.vehicleJourney, {company: undefined}) + newModalProps = _.assign({}, state.modalProps, {vehicleJourney}) return _.assign({}, state, {modalProps: newModalProps}) case 'SELECT_TT_CALENDAR_MODAL': newModalProps = _.assign({}, state.modalProps, {selectedTimetable : action.selectedItem}) return _.assign({}, state, {modalProps: newModalProps}) + case 'SELECT_PURCHASE_WINDOW_MODAL': + newModalProps = _.assign({}, state.modalProps, {selectedPurchaseWindow : action.selectedItem}) + return _.assign({}, state, {modalProps: newModalProps}) case 'ADD_SELECTED_TIMETABLE': if(state.modalProps.selectedTimetable){ newModalProps = JSON.parse(JSON.stringify(state.modalProps)) @@ -73,6 +104,14 @@ export default function modal(state = {}, action) { } return _.assign({}, state, {modalProps: newModalProps}) } + case 'ADD_SELECTED_PURCHASE_WINDOW': + if(state.modalProps.selectedPurchaseWindow){ + newModalProps = JSON.parse(JSON.stringify(state.modalProps)) + if (!_.find(newModalProps.purchase_windows, newModalProps.selectedPurchaseWindow)){ + newModalProps.purchase_windows.push(newModalProps.selectedPurchaseWindow) + } + return _.assign({}, state, {modalProps: newModalProps}) + } case 'DELETE_CALENDAR_MODAL': newModalProps = JSON.parse(JSON.stringify(state.modalProps)) let timetablesModal = state.modalProps.timetables.slice(0) @@ -92,19 +131,32 @@ export default function modal(state = {}, action) { newModalProps.vehicleJourneys = vehicleJourneysModal newModalProps.timetables = timetablesModal return _.assign({}, state, {modalProps: newModalProps}) + case 'DELETE_PURCHASE_WINDOW_MODAL': + newModalProps = JSON.parse(JSON.stringify(state.modalProps)) + let purchase_windows = state.modalProps.purchase_windows.slice(0) + purchase_windows.map((tt, i) =>{ + if(tt == action.purchaseWindow){ + purchase_windows.splice(i, 1) + } + }) + vehicleJourneysModal = state.modalProps.vehicleJourneys.slice(0) + vehicleJourneysModal.map((vj) =>{ + vj.purchase_windows.map((tt, i) =>{ + if (_.isEqual(tt, action.purchaseWindow)){ + vj.purchase_windows.splice(i, 1) + } + }) + }) + newModalProps.vehicleJourneys = vehicleJourneysModal + newModalProps.purchase_windows = purchase_windows + return _.assign({}, state, {modalProps: newModalProps}) case 'CREATE_VEHICLEJOURNEY_MODAL': let selectedJP = {} if (window.jpOrigin){ let stopAreas = _.map(window.jpOriginStopPoints, (sa, i) =>{ return _.assign({}, {stop_area_short_description : {id : sa.stop_area_id}}) }) - selectedJP = { - id: window.jpOrigin.id, - name: window.jpOrigin.name, - published_name: window.jpOrigin.published_name, - objectid: window.jpOrigin.objectid, - stop_areas: stopAreas - } + selectedJP = _.assign({}, window.jpOrigin, {stop_areas: stopAreas}) } return { type: 'create', @@ -135,4 +187,4 @@ export default function modal(state = {}, action) { default: return state } -}
\ No newline at end of file +} diff --git a/app/javascript/vehicle_journeys/reducers/returnVehicleJourneys.js b/app/javascript/vehicle_journeys/reducers/returnVehicleJourneys.js new file mode 100644 index 000000000..db3c71d17 --- /dev/null +++ b/app/javascript/vehicle_journeys/reducers/returnVehicleJourneys.js @@ -0,0 +1,11 @@ +import _ from 'lodash' +import actions from '../actions' + +export default function returnVehicleJourneys(state = [], action) { + switch (action.type) { + case 'RECEIVE_RETURN_VEHICLE_JOURNEYS': + return [...action.json] + default: + return state + } +} diff --git a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js index 136e1b41a..1a15ec46d 100644 --- a/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js +++ b/app/javascript/vehicle_journeys/reducers/vehicleJourneys.js @@ -9,29 +9,68 @@ const vehicleJourney= (state = {}, action, keep) => { return _.assign({}, state, {selected: false}) case 'ADD_VEHICLEJOURNEY': let pristineVjasList = [] + let prevSp = action.stopPointsList[0] + let current_time = { + hour: 0, + minute: 0 + } + if(action.data["start_time.hour"] && action.data["start_time.minute"] && action.selectedJourneyPattern.full_schedule){ + current_time.hour = parseInt(action.data["start_time.hour"].value) + current_time.minute = parseInt(action.data["start_time.minute"].value) || 0 + } _.each(action.stopPointsList, (sp) =>{ + if(action.selectedJourneyPattern.full_schedule && action.selectedJourneyPattern.costs && action.selectedJourneyPattern.costs[prevSp.stop_area_id + "-" + sp.stop_area_id]){ + let delta = parseInt(action.selectedJourneyPattern.costs[prevSp.stop_area_id + "-" + sp.stop_area_id].time) + let delta_hour = parseInt(delta/60) + let delta_minute = delta - 60*delta_hour + current_time.hour += delta_hour + current_time.minute += delta_minute + let extra_hours = parseInt(current_time.minute/60) + current_time.hour += extra_hours + current_time.minute -= extra_hours*60 + current_time.hour = current_time.hour % 24 + prevSp = sp + } + let offsetHours = sp.time_zone_offset / 3600 + let offsetminutes = sp.time_zone_offset/60 - 60*offsetHours let newVjas = { delta: 0, departure_time:{ - hour: '00', - minute: '00' + hour: (24 + current_time.hour + offsetHours) % 24, + minute: current_time.minute + offsetminutes }, arrival_time:{ - hour: '00', - minute: '00' + hour: (24 + current_time.hour + offsetHours) % 24, + minute: current_time.minute + offsetminutes }, stop_point_objectid: sp.object_id, stop_area_cityname: sp.city_name, dummy: true } + if(current_time.hour + offsetHours > 24){ + newVjas.departure_day_offset = 1 + newVjas.arrival_day_offset = 1 + } + if(current_time.hour + offsetHours < 0){ + newVjas.departure_day_offset = -1 + newVjas.arrival_day_offset = -1 + } + _.each(action.selectedJourneyPattern.stop_areas, (jp) =>{ if (jp.stop_area_short_description.id == sp.id){ newVjas.dummy = false return } }) + + if(newVjas.dummy){ + newVjas.departure_time = {hour: "00", minute: "00"} + newVjas.arrival_time = {hour: "00", minute: "00"} + } pristineVjasList.push(newVjas) + }) + return { company: action.selectedCompany, journey_pattern: action.selectedJourneyPattern, @@ -41,11 +80,13 @@ const vehicleJourney= (state = {}, action, keep) => { short_id: '', footnotes: [], time_tables: [], + purchase_windows: [], vehicle_journey_at_stops: pristineVjasList, selected: false, deletable: false, transport_mode: window.transportMode ? window.transportMode : 'undefined', - transport_submode: window.transportSubmode ? window.transportSubmode : 'undefined' + transport_submode: window.transportSubmode ? window.transportSubmode : 'undefined', + custom_fields: action.data.custom_fields } case 'DUPLICATE_VEHICLEJOURNEY': case 'SHIFT_VEHICLEJOURNEY': @@ -79,18 +120,12 @@ const vehicleJourney= (state = {}, action, keep) => { if (action.isDeparture){ newSchedule.departure_time[action.timeUnit] = actions.pad(action.val, action.timeUnit) if(!action.isArrivalsToggled) - newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit) - newSchedule = actions.getDelta(newSchedule) - if(newSchedule.delta < 0){ - return vjas - } + newSchedule.arrival_time[action.timeUnit] = newSchedule.departure_time[action.timeUnit] + newSchedule = actions.adjustSchedule(action, newSchedule) return _.assign({}, state.vehicle_journey_at_stops[action.subIndex], {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta}) }else{ newSchedule.arrival_time[action.timeUnit] = actions.pad(action.val, action.timeUnit) - newSchedule = actions.getDelta(newSchedule) - if(newSchedule.delta < 0){ - return vjas - } + newSchedule = actions.adjustSchedule(action, newSchedule) return _.assign({}, state.vehicle_journey_at_stops[action.subIndex], {arrival_time: newSchedule.arrival_time, departure_time: newSchedule.departure_time, delta: newSchedule.delta}) } }else{ @@ -131,6 +166,7 @@ export default function vehicleJourneys(state = [], action) { company: action.selectedCompany, published_journey_name: action.data.published_journey_name.value, published_journey_identifier: action.data.published_journey_identifier.value, + custom_fields: action.data.custom_fields, }) }else{ return vj @@ -161,6 +197,21 @@ export default function vehicleJourneys(state = [], action) { return vj } }) + case 'EDIT_VEHICLEJOURNEYS_PURCHASE_WINDOWS': + let newWindows = JSON.parse(JSON.stringify(action.purchase_windows)) + return state.map((vj,i) =>{ + if(vj.selected){ + let updatedVJ = _.assign({}, vj) + action.vehicleJourneys.map((vjm, j) =>{ + if(vj.objectid == vjm.objectid){ + updatedVJ.purchase_windows = newWindows + } + }) + return updatedVJ + }else{ + return vj + } + }) case 'SHIFT_VEHICLEJOURNEY': return state.map((vj, i) => { if (vj.selected){ @@ -222,7 +273,9 @@ export default function vehicleJourneys(state = [], action) { return vj } }) + case 'DID_VALIDATE_VEHICLE_JOURNEYS': + return [...action.vehicleJourneys] default: return state } -}
\ No newline at end of file +} diff --git a/app/jobs/mailer_job.rb b/app/jobs/mailer_job.rb index 3918745b8..eb3250a27 100644 --- a/app/jobs/mailer_job.rb +++ b/app/jobs/mailer_job.rb @@ -1,6 +1,9 @@ class MailerJob < ActiveJob::Base # No need to specify queue it's already used mailers queue + # This job will be retried, unlike Sidekiq jobs which are configured + # to not retry + def perform klass, action, params klass.constantize.public_send(action, *params).deliver_later end diff --git a/app/models/calendar.rb b/app/models/calendar.rb index b2e73929f..84b569ab4 100644 --- a/app/models/calendar.rb +++ b/app/models/calendar.rb @@ -3,176 +3,115 @@ require_relative 'calendar/date_value' require_relative 'calendar/period' class Calendar < ActiveRecord::Base - has_paper_trail + include DateSupport + include PeriodSupport + include ApplicationDaysSupport + include TimetableSupport + + has_paper_trail class_name: 'PublicVersion' belongs_to :organisation - has_many :time_tables + belongs_to :workgroup - validates_presence_of :name, :short_name, :organisation + validates_presence_of :name, :short_name, :organisation, :workgroup validates_uniqueness_of :short_name - after_initialize :init_dates_and_date_ranges + has_many :time_tables scope :contains_date, ->(date) { where('date ? = any (dates) OR date ? <@ any (date_ranges)', date, date) } - def init_dates_and_date_ranges - self.dates ||= [] - self.date_ranges ||= [] - end + after_initialize :set_defaults def self.ransackable_scopes(auth_object = nil) [:contains_date] end - def convert_to_time_table - Chouette::TimeTable.new.tap do |tt| - self.dates.each do |d| - tt.dates << Chouette::TimeTableDate.new(date: d, in_out: true) - end - self.periods.each do |p| - tt.periods << Chouette::TimeTablePeriod.new(period_start: p.begin, period_end: p.end) - end - tt.int_day_types = 508 - end + def self.state_permited_attributes item + {name: item["comment"]} end - - ### Calendar::Period - # Required by coocon - def build_period - Calendar::Period.new + def set_defaults + self.excluded_dates ||= [] + self.int_day_types ||= EVERYDAY end - def periods - @periods ||= init_periods + def human_attribute_name(*args) + self.class.human_attribute_name(*args) end - def init_periods - (date_ranges || []) - .each_with_index - .map( &Calendar::Period.method(:from_range) ) + def shortcuts_update(date=nil) end - private :init_periods - - validate :validate_periods - def validate_periods - periods_are_valid = periods.all?(&:valid?) - - periods.each do |period| - if period.intersect?(periods) - period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) - periods_are_valid = false + def convert_to_time_table + Chouette::TimeTable.new.tap do |tt| + self.dates.each do |d| + tt.dates << Chouette::TimeTableDate.new(date: d, in_out: true) end - end - - unless periods_are_valid - errors.add(:periods, :invalid) + self.periods.each do |p| + tt.periods << Chouette::TimeTablePeriod.new(period_start: p.begin, period_end: p.end) + end + tt.int_day_types = self.int_day_types end end - def flatten_date_array attributes, key - date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } - Date.new(*date_int) + def include_in_dates?(day) + self.dates.include? day end - def periods_attributes=(attributes = {}) - @periods = [] - attributes.each do |index, period_attribute| - # Convert date_select to date - ['begin', 'end'].map do |attr| - period_attribute[attr] = flatten_date_array(period_attribute, attr) - end - period = Calendar::Period.new(period_attribute.merge(id: index)) - @periods << period unless period.marked_for_destruction? - end - - date_ranges_will_change! + def excluded_date?(day) + self.excluded_dates.include? day end - before_validation :fill_date_ranges - - def fill_date_ranges - if @periods - self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) + def update_in_out date, in_out + if in_out + self.excluded_dates.delete date + self.dates << date unless include_in_dates?(date) + else + self.dates.delete date + self.excluded_dates << date unless excluded_date?(date) end + date end - after_save :clear_periods - - def clear_periods - @periods = nil + def included_days + dates end - private :clear_periods - - ### Calendar::DateValue - - # Required by coocon - def build_date_value - Calendar::DateValue.new + def excluded_days + excluded_dates end - def date_values - @date_values ||= init_date_values + def saved_dates + Hash[*self.dates.each_with_index.to_a.map(&:reverse).flatten] end - def init_date_values - if dates - dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } - else - [] + def all_dates + (dates + excluded_dates).sort.each_with_index.map do |d, i| + OpenStruct.new(id: i, date: d, in_out: include_in_dates?(d)) end end - private :init_date_values - validate :validate_date_values - - def validate_date_values - date_values_are_valid = date_values.all?(&:valid?) - - date_values.each do |date_value| - if date_values.count { |d| d.value == date_value.value } > 1 - date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) - date_values_are_valid = false - end - date_ranges.each do |date_range| - if date_range.cover? date_value.value - date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) - date_values_are_valid = false - end - end - end - - unless date_values_are_valid - errors.add(:date_values, :invalid) - end + def find_date_by_id id + self.dates[id] end - def date_values_attributes=(attributes = {}) - @date_values = [] - attributes.each do |index, date_value_attribute| - date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') - date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) - @date_values << date_value unless date_value.marked_for_destruction? - end - - dates_will_change! + def destroy_date date + self.dates -= [date] end - before_validation :fill_dates - - def fill_dates - if @date_values - self.dates = @date_values.map(&:value).compact.sort - end + def create_date in_out:, date: + update_in_out date, in_out end - after_save :clear_date_values - - def clear_date_values - @date_values = nil + def find_period_by_id id + self.periods.find{|p| p.id == id} end - private :clear_date_values + def build_period + self.periods << Calendar::Period.new(id: self.periods.count + 1) + self.periods.last + end + def destroy_period period + @periods = self.periods.select{|p| p.end != period.end || p.begin != period.begin} + end end diff --git a/app/models/calendar/period.rb b/app/models/calendar/period.rb index 1c423dfcc..8b3e4109b 100644 --- a/app/models/calendar/period.rb +++ b/app/models/calendar/period.rb @@ -1,5 +1,5 @@ class Calendar < ActiveRecord::Base - + class Period include ActiveAttr::Model @@ -10,6 +10,11 @@ class Calendar < ActiveRecord::Base validates_presence_of :begin, :end validate :check_end_greather_than_begin + alias_method :period_start, :begin + alias_method :period_end, :end + alias_method :period_start=, :begin= + alias_method :period_end=, :end= + def check_end_greather_than_begin if self.begin && self.end && self.begin >= self.end errors.add(:base, I18n.t('calendars.errors.short_period')) diff --git a/app/models/chouette/area_type.rb b/app/models/chouette/area_type.rb new file mode 100644 index 000000000..e17d2ee8d --- /dev/null +++ b/app/models/chouette/area_type.rb @@ -0,0 +1,55 @@ +class Chouette::AreaType + include Comparable + + COMMERCIAL = %i(zdep zder zdlp zdlr lda gdl).freeze + NON_COMMERCIAL = %i(deposit border service_area relief other).freeze + ALL = COMMERCIAL + NON_COMMERCIAL + + @@commercial = COMMERCIAL + @@non_commercial = NON_COMMERCIAL + @@all = ALL + mattr_accessor :all, :commercial, :non_commercial + + def self.commercial=(values) + @@commercial = COMMERCIAL & values + reset_caches! + end + + def self.non_commercial=(values) + @@non_commercial = NON_COMMERCIAL & values + reset_caches! + end + + @@instances = {} + def self.find(code) + return unless code + + code = code.to_sym + @@instances[code] ||= new(code) if ALL.include? code + end + + def self.reset_caches! + @@all = @@commercial + @@non_commercial + @@instances = {} + @@options = {} + end + + def self.options(kind=:all) + @@options ||= {} + @@options[kind] ||= self.send(kind).map { |c| find(c) }.map { |t| [ t.label, t.code ] } + end + + attr_reader :code + def initialize(code) + @code = code + end + + def <=>(other) + all.index(code) <=> all.index(other.code) + end + + def label + I18n.translate code, scope: 'area_types.label' + end + +end diff --git a/app/models/chouette/company.rb b/app/models/chouette/company.rb index 12b21e347..b3d40ab96 100644 --- a/app/models/chouette/company.rb +++ b/app/models/chouette/company.rb @@ -3,7 +3,7 @@ module Chouette include CompanyRestrictions include LineReferentialSupport include ObjectidSupport - has_paper_trail + has_paper_trail class_name: 'PublicVersion' has_many :lines diff --git a/app/models/chouette/journey_pattern.rb b/app/models/chouette/journey_pattern.rb index a62da6353..aa9fdb810 100644 --- a/app/models/chouette/journey_pattern.rb +++ b/app/models/chouette/journey_pattern.rb @@ -27,7 +27,7 @@ module Chouette def checksum_attributes values = self.slice(*['name', 'published_name', 'registration_number']).values - values << self.stop_points.map(&:stop_area).map(&:user_objectid) + values << self.stop_points.sort_by(&:position).map(&:stop_area).map(&:user_objectid) values.flatten end @@ -40,7 +40,8 @@ module Chouette # Update attributes and stop_points associations jp.update_attributes(state_permited_attributes(item)) unless item['new_record'] jp.state_stop_points_update(item) if !jp.errors.any? && jp.persisted? - item['errors'] = jp.errors if jp.errors.any? + item['errors'] = jp.errors if jp.errors.any? + item['checksum'] = jp.checksum end if state.any? {|item| item['errors']} @@ -57,21 +58,23 @@ module Chouette { name: item['name'], published_name: item['published_name'], - registration_number: item['registration_number'] + registration_number: item['registration_number'], + costs: item['costs'] } end def self.state_create_instance route, item # Flag new record, so we can unset object_id if transaction rollback jp = route.journey_patterns.create(state_permited_attributes(item)) - # FIXME # DefaultAttributesSupport will trigger some weird validation on after save # wich will call to valid?, wich will populate errors # In this case, we mark jp to be valid if persisted? return true jp.errors.clear if jp.persisted? + jp.after_commit_objectid item['object_id'] = jp.objectid + item['short_id'] = jp.get_objectid.short_id item['new_record'] = true jp end @@ -145,5 +148,27 @@ module Chouette vjas.destroy end end + + def costs + read_attribute(:costs) || {} + end + + def costs_between start, finish + key = "#{start.stop_area_id}-#{finish.stop_area_id}" + costs[key]&.symbolize_keys || {} + end + + def full_schedule? + full = true + stop_points.order(:position).inject(nil) do |start, finish| + next finish unless start.present? + costs = costs_between(start, finish) + full = false unless costs.present? + full = false unless costs[:distance] && costs[:distance] > 0 + full = false unless costs[:time] && costs[:time] > 0 + finish + end + full + end end end diff --git a/app/models/chouette/line.rb b/app/models/chouette/line.rb index 93d4f5e8b..ba2e2755d 100644 --- a/app/models/chouette/line.rb +++ b/app/models/chouette/line.rb @@ -1,6 +1,6 @@ module Chouette class Line < Chouette::ActiveRecord - has_paper_trail + has_paper_trail class_name: 'PublicVersion' include LineRestrictions include LineReferentialSupport include ObjectidSupport @@ -21,6 +21,7 @@ module Chouette has_many :journey_patterns, :through => :routes has_many :vehicle_journeys, :through => :journey_patterns has_many :routing_constraint_zones, through: :routes + has_many :time_tables, -> { distinct }, :through => :vehicle_journeys has_and_belongs_to_many :group_of_lines, :class_name => 'Chouette::GroupOfLine', :order => 'group_of_lines.name' @@ -79,5 +80,16 @@ module Chouette line_referential.companies.where(id: ([company_id] + Array(secondary_company_ids)).compact) end + def deactivate! + update_attribute :deactivated, true + end + + def activate! + update_attribute :deactivated, false + end + + def activated? + !deactivated + end end end diff --git a/app/models/chouette/network.rb b/app/models/chouette/network.rb index 9b3f2fe29..6843c69ad 100644 --- a/app/models/chouette/network.rb +++ b/app/models/chouette/network.rb @@ -1,6 +1,6 @@ module Chouette class Network < Chouette::ActiveRecord - has_paper_trail + has_paper_trail class_name: 'PublicVersion' include NetworkRestrictions include LineReferentialSupport include ObjectidSupport diff --git a/app/models/chouette/purchase_window.rb b/app/models/chouette/purchase_window.rb new file mode 100644 index 000000000..334493015 --- /dev/null +++ b/app/models/chouette/purchase_window.rb @@ -0,0 +1,45 @@ +require 'range_ext' +require_relative '../calendar/period' + +module Chouette + class PurchaseWindow < Chouette::TridentActiveRecord + # include ChecksumSupport + include ObjectidSupport + include PeriodSupport + include ChecksumSupport + extend Enumerize + + enumerize :color, in: %w(#9B9B9B #FFA070 #C67300 #7F551B #41CCE3 #09B09C #3655D7 #6321A0 #E796C6 #DD2DAA) + + has_paper_trail + belongs_to :referential + has_and_belongs_to_many :vehicle_journeys, :class_name => 'Chouette::VehicleJourney' + + validates_presence_of :name, :referential + + scope :contains_date, ->(date) { where('date ? <@ any (date_ranges)', date) } + scope :overlap_dates, ->(date_range) { where('daterange(?, ?) && any (date_ranges)', date_range.first, date_range.last + 1.day) } + + def self.ransackable_scopes(auth_object = nil) + [:contains_date] + end + + def self.colors_i18n + Hash[*color.values.map{|c| [I18n.t("enumerize.purchase_window.color.#{c[1..-1]}"), c]}.flatten] + end + + def local_id + "IBOO-#{self.referential.id}-#{self.id}" + end + + def checksum_attributes + attrs = ['name', 'color', 'referential_id'] + ranges_attrs = date_ranges.map{|r| [r.first, r.last]}.flatten.sort + self.slice(*attrs).values + ranges_attrs + end + + # def checksum_attributes + # end + + end +end diff --git a/app/models/chouette/route.rb b/app/models/chouette/route.rb index 5c0ad24a1..5cc5d8b0d 100644 --- a/app/models/chouette/route.rb +++ b/app/models/chouette/route.rb @@ -133,7 +133,7 @@ module Chouette def checksum_attributes values = self.slice(*['name', 'published_name', 'wayback']).values values.tap do |attrs| - attrs << self.stop_points.map{|sp| "#{sp.stop_area.user_objectid}#{sp.for_boarding}#{sp.for_alighting}" }.join + attrs << self.stop_points.sort_by(&:position).map{|sp| [sp.stop_area.user_objectid, sp.for_boarding, sp.for_alighting]} attrs << self.routing_constraint_zones.map(&:checksum) end end diff --git a/app/models/chouette/routing_constraint_zone.rb b/app/models/chouette/routing_constraint_zone.rb index fcf47f154..58703598e 100644 --- a/app/models/chouette/routing_constraint_zone.rb +++ b/app/models/chouette/routing_constraint_zone.rb @@ -7,7 +7,7 @@ module Chouette belongs_to :route has_array_of :stop_points, class_name: 'Chouette::StopPoint' - validates_presence_of :name, :stop_points, :route + validates_presence_of :name, :stop_points, :route_id # validates :stop_point_ids, length: { minimum: 2, too_short: I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.not_enough_stop_points') } validate :stop_points_belong_to_route, :not_all_stop_points_selected @@ -25,14 +25,20 @@ module Chouette end def checksum_attributes - self.stop_points.map(&:stop_area).map(&:user_objectid) + [ + self.stop_points.map(&:stop_area).map(&:user_objectid) + ] end def stop_points_belong_to_route + return unless route + errors.add(:stop_point_ids, I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.stop_points_not_from_route')) unless stop_points.all? { |sp| route.stop_points.include? sp } end def not_all_stop_points_selected + return unless route + errors.add(:stop_point_ids, I18n.t('activerecord.errors.models.routing_constraint_zone.attributes.stop_points.all_stop_points_selected')) if stop_points.length == route.stop_points.length end diff --git a/app/models/chouette/stop_area.rb b/app/models/chouette/stop_area.rb index cc7170728..bb8747faa 100644 --- a/app/models/chouette/stop_area.rb +++ b/app/models/chouette/stop_area.rb @@ -2,14 +2,17 @@ require 'geokit' require 'geo_ruby' module Chouette class StopArea < Chouette::ActiveRecord - has_paper_trail + has_paper_trail class_name: 'PublicVersion' include ProjectionFields include StopAreaRestrictions include StopAreaReferentialSupport include ObjectidSupport extend Enumerize - enumerize :area_type, in: %i(zdep zder zdlp zdlr lda) + enumerize :area_type, in: Chouette::AreaType::ALL + enumerize :kind, in: %i(commercial non_commercial) + + AVAILABLE_LOCALIZATIONS = %i(gb nl de fr it es) with_options dependent: :destroy do |assoc| assoc.has_many :stop_points @@ -31,6 +34,7 @@ module Chouette validates_format_of :registration_number, :with => %r{\A[\d\w_\-]+\Z}, :allow_blank => true validates_presence_of :name + validates_presence_of :kind validates_presence_of :latitude, :if => :longitude validates_presence_of :longitude, :if => :latitude validates_numericality_of :latitude, :less_than_or_equal_to => 90, :greater_than_or_equal_to => -90, :allow_nil => true @@ -39,11 +43,36 @@ module Chouette validates_format_of :coordinates, :with => %r{\A *-?(0?[0-9](\.[0-9]*)?|[0-8][0-9](\.[0-9]*)?|90(\.[0]*)?) *\, *-?(0?[0-9]?[0-9](\.[0-9]*)?|1[0-7][0-9](\.[0-9]*)?|180(\.[0]*)?) *\Z}, :allow_nil => true, :allow_blank => true validates_format_of :url, :with => %r{\Ahttps?:\/\/([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?\Z}, :allow_nil => true, :allow_blank => true + validates_numericality_of :waiting_time, greater_than_or_equal_to: 0, only_integer: true, if: :waiting_time + validate :parent_area_type_must_be_greater + validate :area_type_of_right_kind + def self.nullable_attributes [:registration_number, :street_name, :country_code, :fare_code, :nearest_topic_name, :comment, :long_lat_type, :zip_code, :city_name, :url, :time_zone] end + def localized_names + val = read_attribute(:localized_names) || {} + Hash[*AVAILABLE_LOCALIZATIONS.map{|k| [k, val[k.to_s]]}.flatten] + end + + def parent_area_type_must_be_greater + return unless self.parent + + parent_area_type = Chouette::AreaType.find(self.parent.area_type) + if Chouette::AreaType.find(self.area_type) >= parent_area_type + errors.add(:parent_id, I18n.t('stop_areas.errors.parent_area_type', area_type: parent_area_type.label)) + end + end + + def area_type_of_right_kind + return unless self.kind + unless Chouette::AreaType.send(self.kind).map(&:to_s).include?(self.area_type) + errors.add(:area_type, I18n.t('stop_areas.errors.incorrect_kind_area_type')) + end + end + after_update :clean_invalid_access_links before_save :coordinates_to_lat_lng @@ -72,6 +101,10 @@ module Chouette end end + def full_name + "#{name} #{zip_code} #{city_name} - #{user_objectid}" + end + def user_objectid if objectid =~ /^.*:([0-9A-Za-z_-]+):STIF$/ $1 @@ -80,6 +113,8 @@ module Chouette end end + alias_method :local_id, :user_objectid + def children_in_depth return [] if self.children.empty? @@ -196,10 +231,12 @@ module Chouette GeoRuby::SimpleFeatures::Envelope.from_coordinates coordinates end + # DEPRECATED use StopArea#area_type def stop_area_type area_type ? area_type : " " end + # DEPRECATED use StopArea#area_type def stop_area_type=(stop_area_type) self.area_type = (stop_area_type ? stop_area_type.camelcase : nil) end @@ -324,5 +361,41 @@ module Chouette end end + def activated? + deleted_at.nil? + end + + def deactivated? + !activated? + end + + def activate! + update_attribute :deleted_at, nil + end + + def deactivate! + update_attribute :deleted_at, Time.now + end + + def time_zone_offset + return 0 unless time_zone.present? + ActiveSupport::TimeZone[time_zone]&.utc_offset + end + + def country_name + return unless country_code + + country = ISO3166::Country[country_code] + country.translations[I18n.locale.to_s] || country.name + end + + def time_zone_formatted_offset + return nil unless time_zone.present? + ActiveSupport::TimeZone[time_zone]&.formatted_offset + end + + def commercial? + kind == "commercial" + end end end diff --git a/app/models/chouette/time_table.rb b/app/models/chouette/time_table.rb index 74c20f061..15b22b671 100644 --- a/app/models/chouette/time_table.rb +++ b/app/models/chouette/time_table.rb @@ -4,11 +4,13 @@ module Chouette include ChecksumSupport include TimeTableRestrictions include ObjectidSupport + include ApplicationDaysSupport + include TimetableSupport + # FIXME http://jira.codehaus.org/browse/JRUBY-6358 self.primary_key = "id" acts_as_taggable - attr_accessor :monday,:tuesday,:wednesday,:thursday,:friday,:saturday,:sunday attr_accessor :tag_search def self.ransackable_attributes auth_object = nil @@ -40,11 +42,18 @@ module Chouette def checksum_attributes [].tap do |attrs| attrs << self.int_day_types - attrs << self.dates.map(&:checksum).map(&:to_s).sort - attrs << self.periods.map(&:checksum).map(&:to_s).sort + dates = self.dates + dates += TimeTableDate.where(time_table_id: self.id) + attrs << dates.map(&:checksum).map(&:to_s).sort + periods = self.periods + periods += TimeTablePeriod.where(time_table_id: self.id) + attrs << periods.map(&:checksum).map(&:to_s).sort end end + has_checksum_children TimeTableDate + has_checksum_children TimeTablePeriod + def self.object_id_key "Timetable" end @@ -81,72 +90,36 @@ module Chouette end end - def state_update state - update_attributes(self.class.state_permited_attributes(state)) - self.tag_list = state['tags'].collect{|t| t['name']}.join(', ') - self.calendar_id = nil unless state['calendar'] - - days = state['day_types'].split(',') - Date::DAYNAMES.map(&:underscore).each do |name| - prefix = human_attribute_name(name).first(2) - send("#{name}=", days.include?(prefix)) - end - - saved_dates = Hash[self.dates.collect{ |d| [d.id, d.date]}] - cmonth = Date.parse(state['current_periode_range']) - - state['current_month'].each do |d| - date = Date.parse(d['date']) - checked = d['include_date'] || d['excluded_date'] - in_out = d['include_date'] ? true : false - - date_id = saved_dates.key(date) - time_table_date = self.dates.find(date_id) if date_id + def find_date_by_id id + self.dates.find id + end - next if !checked && !time_table_date - # Destroy date if no longer checked - next if !checked && time_table_date.destroy + def destroy_date date + date.destroy + end - # Create new date - unless time_table_date - time_table_date = self.dates.create({in_out: in_out, date: date}) - end - # Update in_out - if in_out != time_table_date.in_out - time_table_date.update_attributes({in_out: in_out}) - end + def update_in_out date, in_out + if in_out != date.in_out + date.update_attributes({in_out: in_out}) end - - self.state_update_periods state['time_table_periods'] - self.save end - def state_update_periods state_periods - state_periods.each do |item| - period = self.periods.find(item['id']) if item['id'] - next if period && item['deleted'] && period.destroy - period ||= self.periods.build - - period.period_start = Date.parse(item['period_start']) - period.period_end = Date.parse(item['period_end']) + def find_period_by_id id + self.periods.find id + end - if period.changed? - period.save - item['id'] = period.id - end - end + def build_period + periods.build + end - state_periods.delete_if {|item| item['deleted']} + def destroy_period period + period.destroy end def self.state_permited_attributes item item.slice('comment', 'color').to_hash end - def presenter - @presenter ||= ::TimeTablePresenter.new( self) - end - def self.start_validity_period [Chouette::TimeTable.minimum(:start_date)].compact.min end @@ -167,20 +140,6 @@ module Chouette self.save end - def month_inspect(date) - (date.beginning_of_month..date.end_of_month).map do |d| - { - day: I18n.l(d, format: '%A'), - date: d.to_s, - wday: d.wday, - wnumber: d.strftime("%W").to_s, - mday: d.mday, - include_date: include_in_dates?(d), - excluded_date: excluded_date?(d) - } - end - end - def save_shortcuts shortcuts_update self.update_column(:start_date, start_date) @@ -311,102 +270,9 @@ module Chouette bounding_max = periods_max_date if periods_max_date && (bounding_max.nil? || (bounding_max < periods_max_date)) end - [bounding_min, bounding_max].compact end - def display_day_types - %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ') - end - - def day_by_mask(flag) - int_day_types & flag == flag - end - - def self.day_by_mask(int_day_types,flag) - int_day_types & flag == flag - end - - - def valid_days - # Build an array with day of calendar week (1-7, Monday is 1). - [].tap do |valid_days| - valid_days << 1 if monday - valid_days << 2 if tuesday - valid_days << 3 if wednesday - valid_days << 4 if thursday - valid_days << 5 if friday - valid_days << 6 if saturday - valid_days << 7 if sunday - end - end - - def self.valid_days(int_day_types) - # Build an array with day of calendar week (1-7, Monday is 1). - [].tap do |valid_days| - valid_days << 1 if day_by_mask(int_day_types,4) - valid_days << 2 if day_by_mask(int_day_types,8) - valid_days << 3 if day_by_mask(int_day_types,16) - valid_days << 4 if day_by_mask(int_day_types,32) - valid_days << 5 if day_by_mask(int_day_types,64) - valid_days << 6 if day_by_mask(int_day_types,128) - valid_days << 7 if day_by_mask(int_day_types,256) - end - end - - def monday - day_by_mask(4) - end - def tuesday - day_by_mask(8) - end - def wednesday - day_by_mask(16) - end - def thursday - day_by_mask(32) - end - def friday - day_by_mask(64) - end - def saturday - day_by_mask(128) - end - def sunday - day_by_mask(256) - end - - def set_day(day,flag) - if day == '1' || day == true - self.int_day_types |= flag - else - self.int_day_types &= ~flag - end - shortcuts_update - end - - def monday=(day) - set_day(day,4) - end - def tuesday=(day) - set_day(day,8) - end - def wednesday=(day) - set_day(day,16) - end - def thursday=(day) - set_day(day,32) - end - def friday=(day) - set_day(day,64) - end - def saturday=(day) - set_day(day,128) - end - def sunday=(day) - set_day(day,256) - end - def effective_days_of_period(period,valid_days=self.valid_days) days = [] period.period_start.upto(period.period_end) do |date| @@ -453,6 +319,17 @@ module Chouette days.sort end + def create_date in_out:, date: + self.dates.create in_out: in_out, date: date + end + + def saved_dates + Hash[self.dates.collect{ |d| [d.id, d.date]}] + end + + def all_dates + dates + end # produce a copy of periods without anyone overlapping or including another def optimize_overlapping_periods @@ -569,5 +446,56 @@ module Chouette tt.comment = I18n.t("activerecord.copy", :name => self.comment) tt end + + def intersect_periods!(mask_periods) + dates.each do |date| + unless mask_periods.any? { |p| p.include? date.date } + dates.delete date + end + end + + periods.each do |period| + mask_periods_with_common_part = mask_periods.select { |p| p.intersect? period.range } + + if mask_periods_with_common_part.empty? + self.periods.delete period + else + mask_periods_with_common_part.each do |mask_period| + intersection = (mask_period & period.range) + period.period_start, period.period_end = intersection.begin, intersection.end + end + end + end + end + + def remove_periods!(removed_periods) + dates.each do |date| + if removed_periods.any? { |p| p.include? date.date } + dates.delete date + end + end + + periods.each do |period| + modified_ranges = removed_periods.inject([period.range]) do |period_ranges, removed_period| + period_ranges.map { |p| p.remove removed_period }.flatten + end + + unless modified_ranges.empty? + modified_ranges.each_with_index do |modified_range, index| + new_period = index == 0 ? period : periods.build + + new_period.period_start, new_period.period_end = + modified_range.min, modified_range.max + end + else + periods.delete period + end + end + end + + def empty? + dates.empty? && periods.empty? + end + end end diff --git a/app/models/chouette/time_table_period.rb b/app/models/chouette/time_table_period.rb index ab3e79d7e..d9b707675 100644 --- a/app/models/chouette/time_table_period.rb +++ b/app/models/chouette/time_table_period.rb @@ -42,5 +42,10 @@ module Chouette def contains?(p) (p.period_start >= self.period_start && p.period_end <= self.period_end) end + + def range + period_start..period_end + end + end -end
\ No newline at end of file +end diff --git a/app/models/chouette/vehicle_journey.rb b/app/models/chouette/vehicle_journey.rb index 247c30668..4a6ba3f75 100644 --- a/app/models/chouette/vehicle_journey.rb +++ b/app/models/chouette/vehicle_journey.rb @@ -1,3 +1,4 @@ +# coding: utf-8 module Chouette class VehicleJourney < Chouette::TridentActiveRecord has_paper_trail @@ -21,8 +22,10 @@ module Chouette belongs_to :company belongs_to :route belongs_to :journey_pattern + has_many :stop_areas, through: :journey_pattern has_and_belongs_to_many :footnotes, :class_name => 'Chouette::Footnote' + has_and_belongs_to_many :purchase_windows, :class_name => 'Chouette::PurchaseWindow' validates_presence_of :route validates_presence_of :journey_pattern @@ -40,6 +43,39 @@ module Chouette before_validation :set_default_values, :calculate_vehicle_journey_at_stop_day_offset + scope :with_stop_area_ids, ->(ids){ + _ids = ids.select(&:present?).map(&:to_i) + if _ids.present? + where("array(SELECT stop_points.stop_area_id::integer FROM stop_points INNER JOIN journey_patterns_stop_points ON journey_patterns_stop_points.stop_point_id = stop_points.id WHERE journey_patterns_stop_points.journey_pattern_id = vehicle_journeys.journey_pattern_id) @> array[?]", _ids) + else + all + end + } + + scope :in_purchase_window, ->(range){ + purchase_windows = Chouette::PurchaseWindow.overlap_dates(range) + sql = purchase_windows.joins(:vehicle_journeys).select('vehicle_journeys.id').uniq.to_sql + where("id IN (#{sql})") + } + + # We need this for the ransack object in the filters + ransacker :purchase_window_date_gt + ransacker :stop_area_ids + + # returns VehicleJourneys with at least 1 day in their time_tables + # included in the given range + def self.with_matching_timetable date_range + out = [] + time_tables = Chouette::TimeTable.where(id: self.joins("INNER JOIN time_tables_vehicle_journeys ON vehicle_journeys.id = time_tables_vehicle_journeys.vehicle_journey_id").pluck('time_tables_vehicle_journeys.time_table_id')).overlapping(date_range) + time_tables = time_tables.select do |time_table| + range = date_range + range = date_range & (time_table.start_date-1.day..time_table.end_date+1.day) || [] if time_table.start_date.present? && time_table.end_date.present? + range.any?{|d| time_table.include_day?(d) } + end + out += time_tables.map{|t| t.vehicle_journey_ids}.flatten + where(id: out) + end + # TODO: Remove this validator # We've eliminated this validation because it prevented vehicle journeys # from being saved with at-stops having a day offset greater than 0, @@ -68,10 +104,14 @@ module Chouette attrs << self.published_journey_identifier attrs << self.try(:company).try(:get_objectid).try(:local_id) attrs << self.footnotes.map(&:checksum).sort - attrs << self.vehicle_journey_at_stops.map(&:checksum).sort + vjas = self.vehicle_journey_at_stops + vjas += VehicleJourneyAtStop.where(vehicle_journey_id: self.id) + attrs << vjas.uniq.sort_by { |s| s.stop_point&.position }.map(&:checksum).sort end end + has_checksum_children VehicleJourneyAtStop + def set_default_values if number.nil? self.number = 0 @@ -117,10 +157,14 @@ module Chouette def update_vjas_from_state state state.each do |vjas| next if vjas["dummy"] + stop_point = Chouette::StopPoint.find_by(objectid: vjas['stop_point_objectid']) + stop_area = stop_point&.stop_area + tz = stop_area&.time_zone + tz = tz && ActiveSupport::TimeZone[tz] params = {}.tap do |el| ['arrival_time', 'departure_time'].each do |field| time = "#{vjas[field]['hour']}:#{vjas[field]['minute']}" - el[field.to_sym] = Time.parse("2000-01-01 #{time}:00 UTC") + el[field.to_sym] = Time.parse("2000-01-01 #{time}:00 #{tz&.formatted_offset || "UTC"}") end end stop = create_or_find_vjas_from_state(vjas) @@ -139,7 +183,7 @@ module Chouette end def update_has_and_belongs_to_many_from_state item - ['time_tables', 'footnotes'].each do |assos| + ['time_tables', 'footnotes', 'purchase_windows'].each do |assos| saved = self.send(assos).map(&:id) (saved - item[assos].map{|t| t['id']}).each do |id| @@ -168,7 +212,8 @@ module Chouette vj.update_attributes(state_permited_attributes(item)) vj.update_has_and_belongs_to_many_from_state(item) - item['errors'] = vj.errors.full_messages.uniq if vj.errors.any? + item['errors'] = vj.errors.full_messages.uniq if vj.errors.any? + item['checksum'] = vj.checksum end # Delete ids of new object from state if we had to rollback @@ -192,16 +237,33 @@ module Chouette def self.state_create_instance route, item # Flag new record, so we can unset object_id if transaction rollback vj = route.vehicle_journeys.create(state_permited_attributes(item)) - item['objectid'] = vj.objectid + vj.after_commit_objectid + item['objectid'] = vj.objectid + item['short_id'] = vj.get_objectid.short_id item['new_record'] = true vj end def self.state_permited_attributes item - attrs = item.slice('published_journey_identifier', 'published_journey_name', 'journey_pattern_id', 'company_id').to_hash - ['company', 'journey_pattern'].map do |association| - attrs["#{association}_id"] = item[association]['id'] if item[association] + attrs = item.slice( + 'published_journey_identifier', + 'published_journey_name', + 'journey_pattern_id', + 'company_id' + ).to_hash + + if item['journey_pattern'] + attrs['journey_pattern_id'] = item['journey_pattern']['id'] end + + attrs['company_id'] = item['company'] ? item['company']['id'] : nil + + attrs["custom_field_values"] = Hash[ + *(item["custom_fields"] || {}) + .map { |k, v| [k, v["value"]] } + .flatten + ] + attrs end @@ -240,12 +302,25 @@ module Chouette end end + def self.custom_fields + CustomField.where(resource_type: self.name.split("::").last) + end + + + def custom_fields + Hash[*self.class.custom_fields.map do |v| + [v.code, v.slice(:code, :name, :field_type, :options).update(value: custom_field_value(v.code))] + end.flatten] + end + + def custom_field_value key + (custom_field_values || {})[key.to_s] + end + def self.matrix(vehicle_journeys) - {}.tap do |hash| - vehicle_journeys.map{ |vj| - vj.vehicle_journey_at_stops.map{ |vjas |hash[ "#{vj.id}-#{vjas.stop_point_id}"] = vjas } - } - end + Hash[*VehicleJourneyAtStop.where(vehicle_journey_id: vehicle_journeys.pluck(:id)).map do |vjas| + [ "#{vjas.vehicle_journey_id}-#{vjas.stop_point_id}", vjas] + end.flatten] end def self.with_stops @@ -304,5 +379,10 @@ module Chouette ') .where('"time_tables_vehicle_journeys"."vehicle_journey_id" IS NULL') end + + def self.lines + lines_query = joins(:route).select("routes.line_id").to_sql + Chouette::Line.where("id IN (#{lines_query})") + end end end diff --git a/app/models/chouette/vehicle_journey_at_stop.rb b/app/models/chouette/vehicle_journey_at_stop.rb index 6f0119e74..eda711ade 100644 --- a/app/models/chouette/vehicle_journey_at_stop.rb +++ b/app/models/chouette/vehicle_journey_at_stop.rb @@ -75,5 +75,42 @@ module Chouette attrs << self.arrival_day_offset.to_s end end + + def departure + format_time departure_time.utc + end + + def arrival + format_time arrival_time.utc + end + + def departure_local_time + local_time departure_time + end + + def arrival_local_time + local_time arrival_time + end + + def departure_local + format_time departure_local_time + end + + def arrival_local + format_time arrival_local_time + end + + private + def local_time time + return unless time + return time unless stop_point&.stop_area&.time_zone.present? + return time unless ActiveSupport::TimeZone[stop_point.stop_area.time_zone].present? + time + ActiveSupport::TimeZone[stop_point.stop_area.time_zone].utc_offset + end + + def format_time time + time.strftime "%H:%M" if time + end + end -end
\ No newline at end of file +end diff --git a/app/models/compliance_check_block.rb b/app/models/compliance_check_block.rb index 05240b428..059547e1b 100644 --- a/app/models/compliance_check_block.rb +++ b/app/models/compliance_check_block.rb @@ -6,8 +6,8 @@ class ComplianceCheckBlock < ActiveRecord::Base has_many :compliance_checks - hstore_accessor :condition_attributes, - transport_mode: :string, - transport_submode: :string + store_accessor :condition_attributes, + :transport_mode, + :transport_submode end diff --git a/app/models/compliance_check_message_export.rb b/app/models/compliance_check_message_export.rb new file mode 100644 index 000000000..04e1a9caa --- /dev/null +++ b/app/models/compliance_check_message_export.rb @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +require "csv" +require "zip" + +class ComplianceCheckMessageExport + include ActiveModel::Validations + include ActiveModel::Conversion + extend ActiveModel::Naming + + attr_accessor :compliance_check_messages + + def initialize(attributes = {}) + attributes.each { |name, value| send("#{name}=", value) } + end + + def persisted? + false + end + + def label(name) + I18n.t "vehicle_journey_exports.label.#{name}" + end + + def column_names + ["criticity", "message key", "resource objectid", "link", "message"] + end + + def to_csv(options = {}) + CSV.generate(options.slice(:col_sep, :quote_char, :force_quotes)) do |csv| + csv << column_names + compliance_check_messages.each do |compliance_check_message| + csv << [compliance_check_message.compliance_check.criticity, *compliance_check_message.message_attributes.values_at('test_id', 'source_objectid'), options[:server_url] + compliance_check_message.message_attributes['source_object_path'], I18n.t("compliance_check_messages.#{compliance_check_message.message_key}", compliance_check_message.message_attributes.deep_symbolize_keys)] + end + end + end + + def to_zip(temp_file,options = {}) + ::Zip::OutputStream.open(temp_file) { |zos| } + ::Zip::File.open(temp_file.path, ::Zip::File::CREATE) do |zipfile| + zipfile.get_output_stream(label("vj_filename")+route.id.to_s+".csv") { |f| f.puts to_csv(options) } + zipfile.get_output_stream(label("tt_filename")+".csv") { |f| f.puts time_tables_to_csv(options) } + zipfile.get_output_stream(label("ftn_filename")+".csv") { |f| f.puts footnotes_to_csv(options) } + end + end + +end diff --git a/app/models/compliance_check_set.rb b/app/models/compliance_check_set.rb index 020100f4a..289fc134f 100644 --- a/app/models/compliance_check_set.rb +++ b/app/models/compliance_check_set.rb @@ -1,6 +1,6 @@ class ComplianceCheckSet < ActiveRecord::Base extend Enumerize - has_paper_trail + has_paper_trail class_name: 'PublicVersion' belongs_to :referential belongs_to :compliance_control_set @@ -19,6 +19,20 @@ class ComplianceCheckSet < ActiveRecord::Base where('created_at BETWEEN :begin AND :end', begin: period_range.begin, end: period_range.end) end + scope :blocked, -> { where('created_at < ? AND status = ?', 4.hours.ago, 'running') } + + def self.finished_statuses + %w(successful failed warning aborted canceled) + end + + def self.abort_old + where( + 'created_at < ? AND status NOT IN (?)', + 4.hours.ago, + finished_statuses + ).update_all(status: 'aborted') + end + def notify_parent if parent # parent.child_change @@ -26,6 +40,14 @@ class ComplianceCheckSet < ActiveRecord::Base end end + def organisation + workbench.organisation + end + + def human_attribute_name(*args) + self.class.human_attribute_name(*args) + end + def update_status statuses = compliance_check_resources.map do |resource| case resource.status diff --git a/app/models/compliance_control.rb b/app/models/compliance_control.rb index 65e22643d..298a63ab9 100644 --- a/app/models/compliance_control.rb +++ b/app/models/compliance_control.rb @@ -3,10 +3,8 @@ class ComplianceControl < ActiveRecord::Base class << self def criticities; %i(warning error) end def default_code; "" end - def prerequisite; I18n.t('compliance_controls.metas.no_prerequisite'); end - def predicate; I18n.t("compliance_controls.#{self.name.underscore}.description") end def dynamic_attributes - hstore_metadata_for_control_attributes.keys + stored_attributes[:control_attributes] || [] end def policy_class @@ -39,7 +37,6 @@ class ComplianceControl < ActiveRecord::Base belongs_to :compliance_control_block enumerize :criticity, in: criticities, scope: true, default: :warning - hstore_accessor :control_attributes, {} validates :criticity, presence: true validates :name, presence: true @@ -66,6 +63,9 @@ def initialize(attributes = {}) self.origin_code ||= self.class.default_code end +def predicate; I18n.t("compliance_controls.#{self.class.name.underscore}.description") end +def prerequisite; I18n.t('compliance_controls.metas.no_prerequisite'); end + end # Ensure STI subclasses are loaded diff --git a/app/models/compliance_control_block.rb b/app/models/compliance_control_block.rb index e27f85ae0..d7d84fd06 100644 --- a/app/models/compliance_control_block.rb +++ b/app/models/compliance_control_block.rb @@ -5,9 +5,9 @@ class ComplianceControlBlock < ActiveRecord::Base belongs_to :compliance_control_set has_many :compliance_controls, dependent: :destroy - hstore_accessor :condition_attributes, - transport_mode: :string, - transport_submode: :string + store_accessor :condition_attributes, + :transport_mode, + :transport_submode validates :transport_mode, presence: true validates :compliance_control_set, presence: true diff --git a/app/models/compliance_control_set.rb b/app/models/compliance_control_set.rb index 41076fefc..c0ea692f2 100644 --- a/app/models/compliance_control_set.rb +++ b/app/models/compliance_control_set.rb @@ -1,5 +1,5 @@ class ComplianceControlSet < ActiveRecord::Base - has_paper_trail + has_paper_trail class_name: 'PublicVersion' belongs_to :organisation has_many :compliance_control_blocks, dependent: :destroy has_many :compliance_controls, dependent: :destroy diff --git a/app/models/concerns/application_days_support.rb b/app/models/concerns/application_days_support.rb new file mode 100644 index 000000000..348436aa4 --- /dev/null +++ b/app/models/concerns/application_days_support.rb @@ -0,0 +1,107 @@ +module ApplicationDaysSupport + extend ActiveSupport::Concern + + MONDAY = 4 + TUESDAY = 8 + WEDNESDAY = 16 + THURSDAY = 32 + FRIDAY = 64 + SATURDAY = 128 + SUNDAY = 256 + EVERYDAY = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY | SATURDAY | SUNDAY + + def display_day_types + %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| self.send(d) }.map{ |d| self.human_attribute_name(d).first(2)}.join(', ') + end + + def day_by_mask(flag) + int_day_types & flag == flag + end + + def self.day_by_mask(int_day_types,flag) + int_day_types & flag == flag + end + + def valid_days + # Build an array with day of calendar week (1-7, Monday is 1). + [].tap do |valid_days| + valid_days << 1 if monday + valid_days << 2 if tuesday + valid_days << 3 if wednesday + valid_days << 4 if thursday + valid_days << 5 if friday + valid_days << 6 if saturday + valid_days << 7 if sunday + end + end + + def valid_day? wday + valid_days.include?(wday) + end + + def self.valid_days(int_day_types) + # Build an array with day of calendar week (1-7, Monday is 1). + [].tap do |valid_days| + valid_days << 1 if day_by_mask(int_day_types,MONDAY) + valid_days << 2 if day_by_mask(int_day_types,TUESDAY) + valid_days << 3 if day_by_mask(int_day_types,WEDNESDAY) + valid_days << 4 if day_by_mask(int_day_types,THURSDAY) + valid_days << 5 if day_by_mask(int_day_types,FRIDAY) + valid_days << 6 if day_by_mask(int_day_types,SATURDAY) + valid_days << 7 if day_by_mask(int_day_types,SUNDAY) + end + end + + def monday + day_by_mask(MONDAY) + end + def tuesday + day_by_mask(TUESDAY) + end + def wednesday + day_by_mask(WEDNESDAY) + end + def thursday + day_by_mask(THURSDAY) + end + def friday + day_by_mask(FRIDAY) + end + def saturday + day_by_mask(SATURDAY) + end + def sunday + day_by_mask(SUNDAY) + end + + def set_day(day,flag) + if day == '1' || day == true + self.int_day_types |= flag + else + self.int_day_types &= ~flag + end + shortcuts_update + end + + def monday=(day) + set_day(day,4) + end + def tuesday=(day) + set_day(day,8) + end + def wednesday=(day) + set_day(day,16) + end + def thursday=(day) + set_day(day,32) + end + def friday=(day) + set_day(day,64) + end + def saturday=(day) + set_day(day,128) + end + def sunday=(day) + set_day(day,256) + end +end diff --git a/app/models/concerns/checksum_support.rb b/app/models/concerns/checksum_support.rb index c95e23bcf..92103798e 100644 --- a/app/models/concerns/checksum_support.rb +++ b/app/models/concerns/checksum_support.rb @@ -3,18 +3,50 @@ module ChecksumSupport SEPARATOR = '|' VALUE_FOR_NIL_ATTRIBUTE = '-' - included do + included do |into| before_save :set_current_checksum_source, :update_checksum + Referential.register_model_with_checksum self + into.extend ClassMethods + end + + module ClassMethods + def has_checksum_children klass, opts={} + parent_class = self + relation = opts[:relation] || self.model_name.singular + klass.after_save do + parent = self.send(relation) + parent&.update_checksum_without_callbacks! + end + end end def checksum_attributes self.attributes.values end + def checksum_replace_nil_or_empty_values values + # Replace empty array by nil & nil by VALUE_FOR_NIL_ATTRIBUTE + values + .map { |x| x.present? && x || VALUE_FOR_NIL_ATTRIBUTE } + .map do |item| + item = + if item.kind_of?(Array) + checksum_replace_nil_or_empty_values(item) + else + item + end + end + end + def current_checksum_source - source = self.checksum_attributes.map{ |x| x unless x.try(:empty?) } - source = source.map{ |x| x || VALUE_FOR_NIL_ATTRIBUTE } - source.map(&:to_s).join(SEPARATOR) + source = checksum_replace_nil_or_empty_values(self.checksum_attributes) + source.map{ |item| + if item.kind_of?(Array) + item.map{ |x| x.kind_of?(Array) ? "(#{x.join(',')})" : x }.join(',') + else + item + end + }.join(SEPARATOR) end def set_current_checksum_source @@ -26,4 +58,20 @@ module ChecksumSupport self.checksum = Digest::SHA256.new.hexdigest(self.checksum_source) end end + + def update_checksum! + set_current_checksum_source + if checksum_source_changed? + update checksum: Digest::SHA256.new.hexdigest(checksum_source) + end + end + + def update_checksum_without_callbacks! + set_current_checksum_source + _checksum = Digest::SHA256.new.hexdigest(checksum_source) + if _checksum != self.checksum + self.checksum = _checksum + self.class.where(id: self.id).update_all(checksum: _checksum) unless self.new_record? + end + end end diff --git a/app/models/concerns/date_support.rb b/app/models/concerns/date_support.rb new file mode 100644 index 000000000..5c66cb1a9 --- /dev/null +++ b/app/models/concerns/date_support.rb @@ -0,0 +1,83 @@ +module DateSupport + extend ActiveSupport::Concern + + included do + after_initialize :init_dates + + def init_dates + self.dates ||= [] + end + + ### Calendar::DateValue + # Required by coocon + def build_date_value + Calendar::DateValue.new + end + + def date_values + @date_values ||= init_date_values + end + + def init_date_values + if dates + dates.each_with_index.map { |d, index| Calendar::DateValue.from_date(index, d) } + else + [] + end + end + private :init_date_values + + validate :validate_date_values + + def validate_date_values + date_values_are_valid = date_values.all?(&:valid?) + + date_values.each do |date_value| + if date_values.count { |d| d.value == date_value.value } > 1 + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_dates')) + date_values_are_valid = false + end + date_ranges.each do |date_range| + if date_range.cover?(date_value.value) + excluded_day = self.respond_to?(:valid_day?) && !self.valid_day?(date_value.value.wday) + unless excluded_day + date_value.errors.add(:base, I18n.t('activerecord.errors.models.calendar.attributes.dates.date_in_date_ranges')) + date_values_are_valid = false + end + end + end + end + + unless date_values_are_valid + errors.add(:date_values, :invalid) + end + end + + def date_values_attributes=(attributes = {}) + @date_values = [] + attributes.each do |index, date_value_attribute| + date_value_attribute['value'] = flatten_date_array(date_value_attribute, 'value') + date_value = Calendar::DateValue.new(date_value_attribute.merge(id: index)) + @date_values << date_value unless date_value.marked_for_destruction? + end + + dates_will_change! + end + + before_validation :fill_dates + + def fill_dates + if @date_values + self.dates = @date_values.map(&:value).compact.sort + end + end + + after_save :clear_date_values + + def clear_date_values + @date_values = nil + end + + private :clear_date_values + end +end diff --git a/app/models/concerns/min_max_values_validation.rb b/app/models/concerns/min_max_values_validation.rb index c177e55ca..eff779d81 100644 --- a/app/models/concerns/min_max_values_validation.rb +++ b/app/models/concerns/min_max_values_validation.rb @@ -2,12 +2,12 @@ module MinMaxValuesValidation extend ActiveSupport::Concern included do + validates_presence_of :minimum, :maximum validate :min_max_values_validation end def min_max_values_validation - return true unless minimum && maximum - return true unless maximum < minimum - errors.add(:min_max_values, I18n.t('compliance_controls.min_max_values', min: minimum, max: maximum)) + return true if (minimum && maximum) && (minimum.to_i < maximum.to_i) + errors.add(:minimum, I18n.t('compliance_controls.min_max_values', min: minimum, max: maximum)) end end diff --git a/app/models/concerns/objectid_support.rb b/app/models/concerns/objectid_support.rb index cec36678e..5d1f1a1c2 100644 --- a/app/models/concerns/objectid_support.rb +++ b/app/models/concerns/objectid_support.rb @@ -26,5 +26,10 @@ module ObjectidSupport def objectid_class get_objectid.try(:class) end + + def raw_objectid + read_attribute(:objectid) + end + end end diff --git a/app/models/concerns/period_support.rb b/app/models/concerns/period_support.rb new file mode 100644 index 000000000..e17451fe4 --- /dev/null +++ b/app/models/concerns/period_support.rb @@ -0,0 +1,80 @@ +module PeriodSupport + extend ActiveSupport::Concern + + included do + after_initialize :init_date_ranges + + def init_date_ranges + self.date_ranges ||= [] + end + + ### Calendar::Period + # Required by coocon + def build_period + Calendar::Period.new + end + + def periods + @periods ||= init_periods + end + + def init_periods + (date_ranges || []) + .each_with_index + .map( &Calendar::Period.method(:from_range) ) + end + private :init_periods + + validate :validate_periods + + def validate_periods + periods_are_valid = periods.all?(&:valid?) + + periods.each do |period| + if period.intersect?(periods) + period.errors.add(:base, I18n.t('calendars.errors.overlapped_periods')) + periods_are_valid = false + end + end + + unless periods_are_valid + errors.add(:periods, :invalid) + end + end + + def flatten_date_array attributes, key + date_int = %w(1 2 3).map {|e| attributes["#{key}(#{e}i)"].to_i } + Date.new(*date_int) + end + + def periods_attributes=(attributes = {}) + @periods = [] + attributes.each do |index, period_attribute| + # Convert date_select to date + ['begin', 'end'].map do |attr| + period_attribute[attr] = flatten_date_array(period_attribute, attr) + end + period = Calendar::Period.new(period_attribute.merge(id: index)) + @periods << period unless period.marked_for_destruction? + end + + date_ranges_will_change! + end + + before_validation :fill_date_ranges + + def fill_date_ranges + if @periods + self.date_ranges = @periods.map(&:range).compact.sort_by(&:begin) + end + end + + after_save :clear_periods + + def clear_periods + @periods = nil + end + + private :clear_periods + end +end diff --git a/app/models/concerns/timetable_support.rb b/app/models/concerns/timetable_support.rb new file mode 100644 index 000000000..5242abc33 --- /dev/null +++ b/app/models/concerns/timetable_support.rb @@ -0,0 +1,149 @@ +module TimetableSupport + extend ActiveSupport::Concern + + def presenter + @presenter ||= ::TimeTablePresenter.new( self) + end + + def periods_max_date + return nil if self.periods.empty? + + min_start = self.periods.map(&:period_start).compact.min + max_end = self.periods.map(&:period_end).compact.max + result = nil + + if max_end && min_start + max_end.downto( min_start) do |date| + if self.valid_days.include?(date.cwday) && !self.excluded_date?(date) + result = date + break + end + end + end + result + end + + def periods_min_date + return nil if self.periods.empty? + + min_start = self.periods.map(&:period_start).compact.min + max_end = self.periods.map(&:period_end).compact.max + result = nil + + if max_end && min_start + min_start.upto(max_end) do |date| + if self.valid_days.include?(date.cwday) && !self.excluded_date?(date) + result = date + break + end + end + end + result + end + + def bounding_dates + bounding_min = self.all_dates.select{|d| d.in_out}.map(&:date).compact.min + bounding_max = self.all_dates.select{|d| d.in_out}.map(&:date).compact.max + + unless self.periods.empty? + bounding_min = periods_min_date if periods_min_date && + (bounding_min.nil? || (periods_min_date < bounding_min)) + + bounding_max = periods_max_date if periods_max_date && + (bounding_max.nil? || (bounding_max < periods_max_date)) + end + + [bounding_min, bounding_max].compact + end + + def month_inspect(date) + (date.beginning_of_month..date.end_of_month).map do |d| + { + day: I18n.l(d, format: '%A'), + date: d.to_s, + wday: d.wday, + wnumber: d.strftime("%W").to_s, + mday: d.mday, + include_date: include_in_dates?(d), + excluded_date: excluded_date?(d) + } + end + end + + def include_in_dates?(day) + self.dates.any?{ |d| d.date === day && d.in_out == true } + end + + def excluded_date?(day) + self.dates.any?{ |d| d.date === day && d.in_out == false } + end + + def include_in_overlap_dates?(day) + return false if self.excluded_date?(day) + + self.all_dates.any?{ |d| d.date === day} \ + && self.periods.any?{ |period| period.period_start <= day && day <= period.period_end && valid_days.include?(day.cwday) } + end + + def include_in_periods?(day) + self.periods.any?{ |period| period.period_start <= day && + day <= period.period_end && + valid_days.include?(day.cwday) && + ! excluded_date?(day) } + end + + def state_update_periods state_periods + state_periods.each do |item| + period = self.find_period_by_id(item['id']) if item['id'] + next if period && item['deleted'] && self.destroy_period(period) + period ||= self.build_period + + period.period_start = Date.parse(item['period_start']) + period.period_end = Date.parse(item['period_end']) + + period.save if period.is_a?(ActiveRecord::Base) && period.changed? + + item['id'] = period.id + end + + state_periods.delete_if {|item| item['deleted']} + end + + def state_update state + update_attributes(self.class.state_permited_attributes(state)) + self.tag_list = state['tags'].collect{|t| t['name']}.join(', ') if state['tags'] + self.calendar_id = nil if self.respond_to?(:calendar_id) && !state['calendar'] + + days = state['day_types'].split(',') + Date::DAYNAMES.map(&:underscore).each do |name| + prefix = human_attribute_name(name).first(2) + send("#{name}=", days.include?(prefix)) + end + + cmonth = Date.parse(state['current_periode_range']) + + state['current_month'].each do |d| + date = Date.parse(d['date']) + checked = d['include_date'] || d['excluded_date'] + in_out = d['include_date'] ? true : false + + date_id = saved_dates.key(date) + time_table_date = self.find_date_by_id(date_id) if date_id + + next if !checked && !time_table_date + # Destroy date if no longer checked + next if !checked && destroy_date(time_table_date) + + # Create new date + unless time_table_date + time_table_date = self.create_date in_out: in_out, date: date + end + # Update in_out + self.update_in_out time_table_date, in_out + end + + self.state_update_periods state['time_table_periods'] + self.save + end + +end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb new file mode 100644 index 000000000..774c8b0f6 --- /dev/null +++ b/app/models/custom_field.rb @@ -0,0 +1,9 @@ +class CustomField < ActiveRecord::Base + + extend Enumerize + belongs_to :workgroup + enumerize :field_type, in: %i{list} + + validates :name, uniqueness: {scope: [:resource_type, :workgroup_id]} + validates :code, uniqueness: {scope: [:resource_type, :workgroup_id], case_sensitive: false} +end diff --git a/app/models/generic_attribute_control/min_max.rb b/app/models/generic_attribute_control/min_max.rb index ab6f546a7..18873b683 100644 --- a/app/models/generic_attribute_control/min_max.rb +++ b/app/models/generic_attribute_control/min_max.rb @@ -1,9 +1,9 @@ module GenericAttributeControl class MinMax < ComplianceControl - hstore_accessor :control_attributes, minimum: :integer, maximum: :integer, target: :string + store_accessor :control_attributes, :minimum, :maximum, :target - validates :minimum, numericality: true, allow_nil: true - validates :maximum, numericality: true, allow_nil: true + validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0 + validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0 validates :target, presence: true include MinMaxValuesValidation diff --git a/app/models/generic_attribute_control/pattern.rb b/app/models/generic_attribute_control/pattern.rb index 3a4a55d5c..7fc008e28 100644 --- a/app/models/generic_attribute_control/pattern.rb +++ b/app/models/generic_attribute_control/pattern.rb @@ -1,6 +1,6 @@ module GenericAttributeControl class Pattern < ComplianceControl - hstore_accessor :control_attributes, pattern: :string, target: :string + store_accessor :control_attributes, :pattern, :target validates :target, presence: true validates :pattern, presence: true diff --git a/app/models/generic_attribute_control/uniqueness.rb b/app/models/generic_attribute_control/uniqueness.rb index f707c944b..82b5c0892 100644 --- a/app/models/generic_attribute_control/uniqueness.rb +++ b/app/models/generic_attribute_control/uniqueness.rb @@ -1,6 +1,6 @@ module GenericAttributeControl class Uniqueness < ComplianceControl - hstore_accessor :control_attributes, target: :string + store_accessor :control_attributes, :target validates :target, presence: true diff --git a/app/models/import.rb b/app/models/import.rb index 19e835986..29aadcd56 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -13,13 +13,14 @@ class Import < ActiveRecord::Base where('started_at BETWEEN :begin AND :end', begin: period_range.begin, end: period_range.end) end + scope :blocked, -> { where('created_at < ? AND status = ?', 4.hours.ago, 'running') } + extend Enumerize enumerize :status, in: %w(new pending successful warning failed running aborted canceled), scope: true, default: :new validates :name, presence: true validates :file, presence: true validates_presence_of :workbench, :creator - validates_format_of :file, with: %r{\.zip\z}i, message: I18n.t('activerecord.errors.models.import.attributes.file.wrong_file_extension') before_create :initialize_fields @@ -43,6 +44,14 @@ class Import < ActiveRecord::Base %w(successful failed warning aborted canceled) end + def self.abort_old + where( + 'created_at < ? AND status NOT IN (?)', + 4.hours.ago, + finished_statuses + ).update_all(status: 'aborted') + end + def notify_parent parent.child_change update(notified_parent_at: DateTime.now) diff --git a/app/models/import_message_export.rb b/app/models/import_message_export.rb index 88d0f27e2..05f8a2cc7 100644 --- a/app/models/import_message_export.rb +++ b/app/models/import_message_export.rb @@ -22,14 +22,14 @@ class ImportMessageExport end def column_names - ["criticity", "message key", "message"] + ["criticity", "message key", "message", "file name", "line", "column"] end def to_csv(options = {}) CSV.generate(options) do |csv| csv << column_names import_messages.each do |import_message| - csv << [import_message.criticity, import_message.message_key, I18n.t("import_messages.compliance_check_messages.#{import_message.message_key}", import_message.message_attributes.deep_symbolize_keys) ] + csv << [import_message.criticity, import_message.message_key, I18n.t("import_messages.#{import_message.message_key}", import_message.message_attributes.deep_symbolize_keys), *import_message.resource_attributes.values_at("filename", "line_number", "column_number") ] end end end diff --git a/app/models/line_control/route.rb b/app/models/line_control/route.rb index b4b2bd9d8..b6c1f3630 100644 --- a/app/models/line_control/route.rb +++ b/app/models/line_control/route.rb @@ -3,6 +3,6 @@ module LineControl def self.default_code; "3-Line-1" end - def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end + def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end end end diff --git a/app/models/merge.rb b/app/models/merge.rb new file mode 100644 index 000000000..62bf581d6 --- /dev/null +++ b/app/models/merge.rb @@ -0,0 +1,435 @@ +class Merge < ActiveRecord::Base + extend Enumerize + + belongs_to :workbench + validates :workbench, presence: true + + enumerize :status, in: %w[new pending successful failed running], default: :new + + has_array_of :referentials, class_name: 'Referential' + + delegate :output, to: :workbench + + after_commit :merge, :on => :create + + def merge + MergeWorker.perform_async(id) + end + + def name + referentials.first(3).map { |r| r.name.truncate(10) }.join(',') + end + + def full_names + referentials.map(&:name).to_sentence + end + + attr_reader :new + + def merge! + update started_at: Time.now, status: :running + + prepare_new + + referentials.each do |referential| + merge_referential referential + end + + save_current + rescue => e + Rails.logger.error "Merge failed: #{e} #{e.backtrace.join("\n")}" + update status: :failed + raise e if Rails.env.test? + ensure + attributes = { ended_at: Time.now } + attributes[:status] = :successful if status == :running + update attributes + end + + def prepare_new + new = + if workbench.output.current + Rails.logger.debug "Clone current output" + Referential.new_from(workbench.output.current, fixme_functional_scope).tap do |clone| + clone.inline_clone = true + end + else + Rails.logger.debug "Create a new output" + # 'empty' one + attributes = { + workbench: workbench, + organisation: workbench.organisation, # TODO could be workbench.organisation by default + } + workbench.output.referentials.new attributes + end + + new.referential_suite = output + new.workbench = workbench + new.organisation = workbench.organisation + new.slug = "output_#{workbench.id}_#{created_at.to_i}" + new.name = I18n.t("merges.referential_name", date: I18n.l(created_at)) + + unless new.valid? + Rails.logger.error "New referential isn't valid : #{new.errors.inspect}" + end + + new.save! + + output.update new: new + @new = new + end + + def merge_referential(referential) + Rails.logger.debug "Merge #{referential.slug}" + + metadata_merger = MetadatasMerger.new new, referential + metadata_merger.merge + + new.metadatas.delete metadata_merger.empty_metadatas + + new.save! + + line_periods = LinePeriods.from_metadatas(referential.metadatas) + + new.switch do + line_periods.each do |line_id, periods| + Rails.logger.debug "Clean data for #{line_id} #{periods.inspect}" + + new.lines.find(line_id).time_tables.find_each do |time_table| + time_table.remove_periods! periods + unless time_table.empty? + time_table.save! + else + time_table.destroy + end + end + end + end + + # let's merge data :) + + # Routes + + # Always the same pattern : + # - load models from original Referential + # - load associated datas (children, checksum for associated models) + # - switch to new Referential + # - enumerate loaded models + # - skip model if its checksum exists "in the same line" + # - prepare attributes for a fresh model + # - remove all primary keys + # - compute an ObjectId (TODO) + # - process children models as nested attributes + # - associated other models (by line/checksum) + # - save! and next one + + referential_routes = referential.switch do + referential.routes.all.to_a + end + + referential_routes_checksums = Hash[referential_routes.map { |r| [ r.id, r.checksum ] }] + + referential_stop_points = referential.switch do + referential.stop_points.all.to_a + end + + referential_stop_points_by_route = referential_stop_points.group_by(&:route_id) + + new.switch do + referential_routes.each do |route| + existing_route = new.routes.find_by line_id: route.line_id, checksum: route.checksum + unless existing_route + objectid = Chouette::Route.where(objectid: route.objectid).exists? ? nil : route.objectid + attributes = route.attributes.merge( + id: nil, + objectid: objectid, + # line_id is the same + # all other primary must be changed + opposite_route_id: nil #FIXME + ) + new_route = new.routes.build attributes + + route_stop_points = referential_stop_points_by_route[route.id] + + # Stop Points + route_stop_points.each do |stop_point| + objectid = Chouette::StopPoint.where(objectid: stop_point.objectid).exists? ? nil : stop_point.objectid + attributes = stop_point.attributes.merge( + id: nil, + route_id: nil, + objectid: objectid, + ) + + new_route.stop_points.build attributes + end + + new_route.save! + + if new_route.checksum != route.checksum + raise "Checksum has changed: #{route.inspect} #{new_route.inspect}" + end + end + end + end + + # JourneyPatterns + + referential_journey_patterns, referential_journey_patterns_stop_areas_objectids = referential.switch do + journey_patterns = referential.journey_patterns.includes(stop_points: :stop_area) + + journey_patterns_stop_areas_objectids = Hash[ + journey_patterns.map do |journey_pattern| + [ journey_pattern.id, journey_pattern.stop_points.map(&:stop_area).map(&:raw_objectid)] + end + ] + + [journey_patterns, journey_patterns_stop_areas_objectids] + end + + referential_journey_patterns_checksums = Hash[referential_journey_patterns.map { |j| [ j.id, j.checksum ] }] + + new.switch do + referential_journey_patterns.each do |journey_pattern| + # find parent route by checksum + # TODO add line_id for security + associated_route_checksum = referential_routes_checksums[journey_pattern.route_id] + existing_associated_route = new.routes.find_by checksum: associated_route_checksum + + existing_journey_pattern = new.journey_patterns.find_by route_id: existing_associated_route.id, checksum: journey_pattern.checksum + + unless existing_journey_pattern + objectid = Chouette::JourneyPattern.where(objectid: journey_pattern.objectid).exists? ? nil : journey_pattern.objectid + attributes = journey_pattern.attributes.merge( + id: nil, + objectid: objectid, + + # all other primary must be changed + route_id: existing_associated_route.id, + + departure_stop_point_id: nil, # FIXME + arrival_stop_point_id: nil + ) + + stop_areas_objectids = referential_journey_patterns_stop_areas_objectids[journey_pattern.id] + + stop_points = existing_associated_route.stop_points.joins(:stop_area).where("stop_areas.objectid": stop_areas_objectids).order(:position) + if stop_points.count != stop_areas_objectids.count + raise "Can't find StopPoints for #{stop_areas_objectids} : #{stop_points.inspect} #{existing_associated_route.stop_points.inspect}" + end + + attributes.merge!(stop_points: stop_points) + + new_journey_pattern = new.journey_patterns.create! attributes + if new_journey_pattern.checksum != journey_pattern.checksum + raise "Checksum has changed for #{journey_pattern.inspect}: #{journey_pattern.checksum_source} #{new_journey_pattern.checksum_source} " + end + end + end + end + + # Vehicle Journeys + + referential_vehicle_journeys = referential.switch do + referential.vehicle_journeys.includes(:vehicle_journey_at_stops).all.to_a + end + + new.switch do + referential_vehicle_journeys.each do |vehicle_journey| + # find parent journey pattern by checksum + # TODO add line_id for security + associated_journey_pattern_checksum = referential_journey_patterns_checksums[vehicle_journey.journey_pattern_id] + existing_associated_journey_pattern = new.journey_patterns.find_by checksum: associated_journey_pattern_checksum + + existing_vehicle_journey = new.vehicle_journeys.find_by journey_pattern_id: existing_associated_journey_pattern.id, checksum: vehicle_journey.checksum + + unless existing_vehicle_journey + objectid = Chouette::VehicleJourney.where(objectid: vehicle_journey.objectid).exists? ? nil : vehicle_journey.objectid + attributes = vehicle_journey.attributes.merge( + id: nil, + objectid: objectid, + + # all other primary must be changed + route_id: existing_associated_journey_pattern.route_id, + journey_pattern_id: existing_associated_journey_pattern.id, + ) + new_vehicle_journey = new.vehicle_journeys.build attributes + + # Create VehicleJourneyAtStops + + vehicle_journey.vehicle_journey_at_stops.each_with_index do |vehicle_journey_at_stop, index| + at_stop_attributes = vehicle_journey_at_stop.attributes.merge( + id: nil, + stop_point_id: existing_associated_journey_pattern.stop_points[index].id + ) + new_vehicle_journey.vehicle_journey_at_stops.build at_stop_attributes + end + + new_vehicle_journey.save! + + if new_vehicle_journey.checksum != vehicle_journey.checksum + raise "Checksum has changed: #{vehicle_journey.checksum_source} #{new_vehicle_journey.checksum_source}" + end + end + + end + end + + # Time Tables + + referential_time_tables_by_id, referential_time_tables_with_lines = referential.switch do + time_tables_by_id = Hash[referential.time_tables.includes(:dates, :periods).all.to_a.map { |t| [t.id, t] }] + + time_tables_with_associated_lines = + referential.time_tables.joins(vehicle_journeys: {route: :line}).pluck("lines.id", :id, "vehicle_journeys.checksum") + + # Because TimeTables will be modified according metadata periods + # we're loading timetables per line (line is associated to a period list) + # + # line_id: [ { time_table.id, vehicle_journey.checksum } ] + time_tables_by_lines = time_tables_with_associated_lines.inject(Hash.new { |h,k| h[k] = [] }) do |hash, row| + hash[row.shift] << {id: row.first, vehicle_journey_checksum: row.second} + hash + end + + [ time_tables_by_id, time_tables_by_lines ] + end + + new.switch do + referential_time_tables_with_lines.each do |line_id, time_tables_properties| + # Because TimeTables will be modified according metadata periods + # we're loading timetables per line (line is associated to a period list) + line = workbench.line_referential.lines.find(line_id) + + time_tables_properties.each do |properties| + time_table = referential_time_tables_by_id[properties[:id]] + + # we can't test if TimeTable already exist by checksum + # because checksum is modified by intersect_periods! + + attributes = time_table.attributes.merge( + id: nil, + comment: "Ligne #{line.name} - #{time_table.comment}", + calendar_id: nil + ) + candidate_time_table = new.time_tables.build attributes + + time_table.dates.each do |date| + date_attributes = date.attributes.merge( + id: nil, + time_table_id: nil + ) + candidate_time_table.dates.build date_attributes + end + time_table.periods.each do |period| + period_attributes = period.attributes.merge( + id: nil, + time_table_id: nil + ) + candidate_time_table.periods.build period_attributes + end + + candidate_time_table.intersect_periods! line_periods.periods(line_id) + + # FIXME + candidate_time_table.set_current_checksum_source + candidate_time_table.update_checksum + + # after intersect_periods!, the checksum is the expected one + # we can search an existing TimeTable + + existing_time_table = line.time_tables.find_by checksum: candidate_time_table.checksum + + unless existing_time_table + objectid = Chouette::TimeTable.where(objectid: time_table.objectid).exists? ? nil : time_table.objectid + candidate_time_table.objectid = objectid + + candidate_time_table.save! + + # Checksum is changed by #intersect_periods + # if new_time_table.checksum != time_table.checksum + # raise "Checksum has changed: #{time_table.checksum_source} #{new_time_table.checksum_source}" + # end + + existing_time_table = candidate_time_table + end + + # associate VehicleJourney + + associated_vehicle_journey = line.vehicle_journeys.find_by!(checksum: properties[:vehicle_journey_checksum]) + associated_vehicle_journey.time_tables << existing_time_table + end + end + end + end + + def save_current + output.update current: new, new: nil + output.current.update referential_suite: output + + referentials.update_all merged_at: created_at, archived_at: created_at + end + + def fixme_functional_scope + if attribute = workbench.organisation.sso_attributes.try(:[], "functional_scope") + JSON.parse(attribute) + end + end + + def child_change + + end + + class MetadatasMerger + + attr_reader :merge_metadatas, :referential + def initialize(merge_referential, referential) + @merge_metadatas = merge_referential.metadatas + @referential = referential + end + + delegate :metadatas, to: :referential, prefix: :referential + + def merge + referential_metadatas.each do |metadata| + merge_one metadata + end + end + + def merged_line_metadatas(line_id) + merge_metadatas.select do |m| + m.line_ids.include? line_id + end + end + + def merge_one(metadata) + metadata.line_ids.each do |line_id| + line_metadatas = merged_line_metadatas(line_id) + + metadata.periodes.each do |period| + line_metadatas.each do |m| + m.periodes = m.periodes.map do |existing_period| + existing_period.remove period + end.flatten + end + + attributes = { + line_ids: [line_id], + periodes: [period], + referential_source_id: referential.id, + created_at: metadata.created_at # TODO check required dates + } + + # line_metadatas should not contain conflicted metadatas + merge_metadatas << ReferentialMetadata.new(attributes) + end + end + end + + def empty_metadatas + merge_metadatas.select { |m| m.periodes.empty? } + end + + + end + +end diff --git a/app/models/organisation.rb b/app/models/organisation.rb index f6fba2d67..da7d1fcf3 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -1,3 +1,4 @@ +# coding: utf-8 class Organisation < ActiveRecord::Base include DataFormatEnumerations @@ -18,36 +19,39 @@ class Organisation < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :code - def self.portail_api_request - conf = Rails.application.config.try(:stif_portail_api) - raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf + class << self - HTTPService.get_json_resource( - host: conf[:url], - path: '/api/v1/organizations', - token: conf[:key]) - end + def portail_api_request + conf = Rails.application.config.try(:stif_portail_api) + raise 'Rails.application.config.stif_portail_api configuration is not defined' unless conf + + HTTPService.get_json_resource( + host: conf[:url], + path: '/api/v1/organizations', + token: conf[:key]) + end - def self.sync_update code, name, scope - org = Organisation.find_or_initialize_by(code: code) - if scope - org.sso_attributes ||= {} - if org.sso_attributes['functional_scope'] != scope - org.sso_attributes['functional_scope'] = scope - # FIXME see #1941 - org.sso_attributes_will_change! + def sync_update code, name, scope + org = Organisation.find_or_initialize_by(code: code) + if scope + org.sso_attributes ||= {} + if org.sso_attributes['functional_scope'] != scope + org.sso_attributes['functional_scope'] = scope + # FIXME see #1941 + org.sso_attributes_will_change! + end end + org.name = name + org.synced_at = Time.now + org.save + org end - org.name = name - org.synced_at = Time.now - org.save - org - end - def self.portail_sync - self.portail_api_request.each do |el| - org = self.sync_update el['code'], el['name'], el['functional_scope'] - puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test? + def portail_sync + portail_api_request.each do |el| + org = self.sync_update el['code'], el['name'], el['functional_scope'] + puts "✓ Organisation #{org.name} has been updated" unless Rails.env.test? + end end end @@ -64,4 +68,16 @@ class Organisation < ActiveRecord::Base raise ActiveRecord::RecordNotFound end + def functional_scope + JSON.parse( (sso_attributes || {}).fetch('functional_scope', '[]') ) + end + + def lines_set + STIF::CodifligneLineId.lines_set_from_functional_scope( functional_scope ) + end + + def has_feature?(feature) + features && features.include?(feature.to_s) + end + end diff --git a/app/models/public_version.rb b/app/models/public_version.rb new file mode 100644 index 000000000..4dbf6ce27 --- /dev/null +++ b/app/models/public_version.rb @@ -0,0 +1,4 @@ +class PublicVersion < PaperTrail::Version + # custom behaviour, e.g: + self.table_name = :'public.versions' +end diff --git a/app/models/referential.rb b/app/models/referential.rb index 851a33653..509e0412f 100644 --- a/app/models/referential.rb +++ b/app/models/referential.rb @@ -13,17 +13,17 @@ class Referential < ActiveRecord::Base validates_uniqueness_of :slug - validates_format_of :slug, :with => %r{\A[a-z][0-9a-z_]+\Z} - validates_format_of :prefix, :with => %r{\A[0-9a-zA-Z_]+\Z} - validates_format_of :upper_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z} - validates_format_of :lower_corner, :with => %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z} + validates_format_of :slug, with: %r{\A[a-z][0-9a-z_]+\Z} + validates_format_of :prefix, with: %r{\A[0-9a-zA-Z_]+\Z} + validates_format_of :upper_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z} + validates_format_of :lower_corner, with: %r{\A-?[0-9]+\.?[0-9]*\,-?[0-9]+\.?[0-9]*\Z} validate :slug_excluded_values attr_accessor :upper_corner attr_accessor :lower_corner has_one :user - has_many :api_keys, :class_name => 'Api::V1::ApiKey', :dependent => :destroy + has_many :api_keys, class_name: 'Api::V1::ApiKey', dependent: :destroy belongs_to :organisation validates_presence_of :organisation @@ -61,6 +61,60 @@ class Referential < ActiveRecord::Base scope :include_metadatas_lines, ->(line_ids) { where('referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids) } scope :order_by_validity_period, ->(dir) { joins(:metadatas).order("unnest(periodes) #{dir}") } scope :order_by_lines, ->(dir) { joins(:metadatas).group("referentials.id").order("sum(array_length(referential_metadata.line_ids,1)) #{dir}") } + scope :not_in_referential_suite, -> { where referential_suite_id: nil } + scope :blocked, -> { where('ready = ? AND created_at < ?', false, 4.hours.ago) } + + def save_with_table_lock_timeout(options = {}) + save_without_table_lock_timeout(options) + rescue ActiveRecord::StatementInvalid => e + if e.message.include?('PG::LockNotAvailable') + raise TableLockTimeoutError.new(e) + else + raise + end + end + + alias_method_chain :save, :table_lock_timeout + + if Rails.env.development? + def self.force_register_models_with_checksum + paths = Rails.application.paths['app/models'].to_a + Rails.application.railties.each do |tie| + next unless tie.respond_to? :paths + paths += tie.paths['app/models'].to_a + end + + paths.each do |path| + next unless File.directory?(path) + Dir.chdir path do + Dir['**/*.rb'].each do |src| + next if src =~ /^concerns/ + # thanks for inconsistent naming ... + if src == "route_control/zdl_stop_area.rb" + RouteControl::ZDLStopArea + next + end + Rails.logger.info "Loading #{src}" + begin + src[0..-4].classify.safe_constantize + rescue => e + Rails.logger.info "Failed: #{e.message}" + nil + end + end + end + end + end + end + + def self.register_model_with_checksum klass + @_models_with_checksum ||= [] + @_models_with_checksum << klass + end + + def self.models_with_checksum + @_models_with_checksum || [] + end def lines if metadatas.blank? @@ -79,7 +133,7 @@ class Referential < ActiveRecord::Base errors.add(:slug,I18n.t("referentials.errors.public_excluded")) end if slug == self.class.connection_config[:username] - errors.add(:slug,I18n.t("referentials.errors.user_excluded", :user => slug)) + errors.add(:slug,I18n.t("referentials.errors.user_excluded", user: slug)) end end end @@ -92,8 +146,12 @@ class Referential < ActiveRecord::Base self.class.human_attribute_name(*args) end - def stop_areas - Chouette::StopArea.all + def full_name + if in_referential_suite? + name + else + "#{self.class.model_name.human.capitalize} #{name}" + end end def access_points @@ -128,6 +186,26 @@ class Referential < ActiveRecord::Base Chouette::RoutingConstraintZone.all end + def purchase_windows + Chouette::PurchaseWindow.all + end + + def routes + Chouette::Route.all + end + + def journey_patterns + Chouette::JourneyPattern.all + end + + def stop_points + Chouette::StopPoint.all + end + + def compliance_check_sets + ComplianceCheckSet.all + end + before_validation :define_default_attributes def define_default_attributes @@ -135,14 +213,26 @@ class Referential < ActiveRecord::Base self.objectid_format ||= workbench.objectid_format if workbench end - def switch + def switch(&block) raise "Referential not created" if new_record? - Apartment::Tenant.switch!(slug) - self + + unless block_given? + Rails.logger.debug "Referential switch to #{slug}" + Apartment::Tenant.switch! slug + self + else + result = nil + Apartment::Tenant.switch slug do + Rails.logger.debug "Referential switch to #{slug}" + result = yield + end + Rails.logger.debug "Referential back" + result + end end def self.new_from(from, functional_scope) - Referential.new( + Referential.new( name: I18n.t("activerecord.copy", name: from.name), slug: "#{from.slug}_clone", prefix: from.prefix, @@ -192,15 +282,29 @@ class Referential < ActiveRecord::Base projection_type || "" end - before_validation :assign_line_and_stop_area_referential, :on => :create, if: :workbench - before_validation :assign_slug, :on => :create - before_validation :assign_prefix, :on => :create + before_validation :assign_line_and_stop_area_referential, on: :create, if: :workbench + before_validation :assign_slug, on: :create + before_validation :assign_prefix, on: :create + + # Lock the `referentials` table to prevent duplicate referentials from being + # created simultaneously in separate transactions. This must be the last hook + # to minimise the duration of the lock. + before_save :lock_table, on: [:create, :update] + before_create :create_schema after_create :clone_schema, if: :created_from before_destroy :destroy_schema before_destroy :destroy_jobs + def referential_read_only? + in_referential_suite? || archived? + end + + def in_referential_suite? + referential_suite_id.present? + end + def in_workbench? workbench_id.present? end @@ -264,7 +368,7 @@ class Referential < ActiveRecord::Base query = "select distinct(public.referential_metadata.referential_id) FROM public.referential_metadata, unnest(line_ids) line, LATERAL unnest(periodes) period WHERE public.referential_metadata.referential_id - IN (SELECT public.referentials.id FROM public.referentials WHERE referentials.workbench_id = #{workbench_id} and referentials.archived_at is null #{not_myself}) + IN (SELECT public.referentials.id FROM public.referentials WHERE referentials.workbench_id = #{workbench_id} and referentials.archived_at is null and referentials.referential_suite_id is null #{not_myself}) AND line in (#{line_ids.join(',')}) and (#{periods_query});" self.class.connection.select_values(query).map(&:to_i) @@ -274,22 +378,45 @@ class Referential < ActiveRecord::Base overlapped_referential_ids.present? end - validate :detect_overlapped_referentials + validate :detect_overlapped_referentials, unless: :in_referential_suite? def detect_overlapped_referentials self.class.where(id: overlapped_referential_ids).each do |referential| + Rails.logger.info "Referential #{referential.id} #{referential.metadatas.inspect} overlaps #{metadatas.inspect}" errors.add :metadatas, I18n.t("referentials.errors.overlapped_referential", :referential => referential.name) end end + + attr_accessor :inline_clone def clone_schema - ReferentialCloning.create(source_referential: created_from, target_referential: self) + cloning = ReferentialCloning.new source_referential: created_from, target_referential: self + + if inline_clone + cloning.clone! + else + cloning.save! + end end def create_schema unless created_from - Apartment::Tenant.create slug - Rails.logger.error( "Schema migrations count for Referential #{slug} " + Referential.connection.select_value("select count(*) from #{slug}.schema_migrations;").to_s ) + report = Benchmark.measure do + Apartment::Tenant.create slug + end + + check_migration_count(report) + end + end + + def check_migration_count(report) + Rails.logger.info("Schema create benchmark: '#{slug}'\t#{report}") + Rails.logger.info("Schema migrations count for Referential #{slug}: #{migration_count || '-'}") + end + + def migration_count + if self.class.connection.table_exists?("#{slug}.schema_migrations") + self.class.connection.select_value("select count(*) from #{slug}.schema_migrations;") end end @@ -373,4 +500,25 @@ class Referential < ActiveRecord::Base not metadatas_overlap? end + def merged? + merged_at.present? + end + + def self.not_merged + where merged_at: nil + end + + def self.mergeable + ready.not_merged.not_in_referential_suite + end + + private + + def lock_table + # No explicit unlock is needed as it will be released at the end of the + # transaction. + ActiveRecord::Base.connection.execute( + 'LOCK public.referentials IN ACCESS EXCLUSIVE MODE' + ) + end end diff --git a/app/models/referential_cloning.rb b/app/models/referential_cloning.rb index 5bf283814..d4b74bd52 100644 --- a/app/models/referential_cloning.rb +++ b/app/models/referential_cloning.rb @@ -2,14 +2,33 @@ class ReferentialCloning < ActiveRecord::Base include AASM belongs_to :source_referential, class_name: 'Referential' belongs_to :target_referential, class_name: 'Referential' - after_commit :perform_clone, :on => :create + after_commit :clone, on: :create - private - def perform_clone + def clone ReferentialCloningWorker.perform_async(id) - # ReferentialCloningWorker.new.perform(id) end + def clone_with_status! + run! + clone! + successful! + rescue Exception => e + Rails.logger.error "Clone failed : #{e}" + Rails.logger.error e.backtrace.join('\n') + failed! + end + + def clone! + report = Benchmark.measure do + AF83::SchemaCloner + .new(source_referential.slug, target_referential.slug) + .clone_schema + end + target_referential.check_migration_count(report) + end + + private + aasm column: :status do state :new, :initial => true state :pending diff --git a/app/models/referential_suite.rb b/app/models/referential_suite.rb index 93c2c3f36..4f825628c 100644 --- a/app/models/referential_suite.rb +++ b/app/models/referential_suite.rb @@ -1,7 +1,7 @@ class ReferentialSuite < ActiveRecord::Base belongs_to :new, class_name: 'Referential' validate def validate_consistent_new - return true if new_id.nil? + return true if new_id.nil? || new.nil? return true if new.referential_suite_id == id errors.add(:inconsistent_new, I18n.t('referential_suites.errors.inconsistent_new', name: new.name)) @@ -9,11 +9,11 @@ class ReferentialSuite < ActiveRecord::Base belongs_to :current, class_name: 'Referential' validate def validate_consistent_current - return true if current_id.nil? + return true if current_id.nil? || current.nil? return true if current.referential_suite_id == id errors.add(:inconsistent_current, I18n.t('referential_suites.errors.inconsistent_current', name: current.name)) end - has_many :referentials + has_many :referentials, -> { order "created_at desc" } end diff --git a/app/models/route_control/opposite_route.rb b/app/models/route_control/opposite_route.rb index d5616ca6f..e0e9572ce 100644 --- a/app/models/route_control/opposite_route.rb +++ b/app/models/route_control/opposite_route.rb @@ -4,6 +4,6 @@ module RouteControl def self.default_code; "3-Route-2" end - def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end + def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end end end diff --git a/app/models/route_control/opposite_route_terminus.rb b/app/models/route_control/opposite_route_terminus.rb index 24c557734..e70d2c702 100644 --- a/app/models/route_control/opposite_route_terminus.rb +++ b/app/models/route_control/opposite_route_terminus.rb @@ -3,6 +3,6 @@ module RouteControl def self.default_code; "3-Route-5" end - def self.prerequisite; I18n.t("compliance_controls.#{self.name.underscore}.prerequisite") end + def prerequisite; I18n.t("compliance_controls.#{self.class.name.underscore}.prerequisite") end end end diff --git a/app/models/user.rb b/app/models/user.rb index 37d35209a..1342f60ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,7 +36,7 @@ class User < ActiveRecord::Base self.name = extra[:full_name] self.email = extra[:email] self.organisation = Organisation.sync_update extra[:organisation_code], extra[:organisation_name], extra[:functional_scope] - self.permissions = Stif::PermissionTranslator.translate(extra[:permissions]) + self.permissions = Stif::PermissionTranslator.translate(extra[:permissions], self.organisation) end def self.portail_api_request diff --git a/app/models/vehicle_journey_control/delta.rb b/app/models/vehicle_journey_control/delta.rb index 1f3a4d492..737b7d78c 100644 --- a/app/models/vehicle_journey_control/delta.rb +++ b/app/models/vehicle_journey_control/delta.rb @@ -1,9 +1,10 @@ module VehicleJourneyControl class Delta < ComplianceControl - hstore_accessor :control_attributes, maximum: :integer + store_accessor :control_attributes, :maximum - validates :maximum, numericality: true, allow_nil: true + validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0 + validates_presence_of :maximum def self.default_code; "3-VehicleJourney-3" end end diff --git a/app/models/vehicle_journey_control/speed.rb b/app/models/vehicle_journey_control/speed.rb index be9f838e4..e5e331b50 100644 --- a/app/models/vehicle_journey_control/speed.rb +++ b/app/models/vehicle_journey_control/speed.rb @@ -1,9 +1,9 @@ module VehicleJourneyControl class Speed < ComplianceControl - hstore_accessor :control_attributes, minimum: :integer, maximum: :integer + store_accessor :control_attributes, :minimum, :maximum - validates :minimum, numericality: true, allow_nil: true - validates :maximum, numericality: true, allow_nil: true + validates_numericality_of :minimum, allow_nil: true, greater_than_or_equal_to: 0 + validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0 include MinMaxValuesValidation def self.default_code; "3-VehicleJourney-2" end diff --git a/app/models/vehicle_journey_control/waiting_time.rb b/app/models/vehicle_journey_control/waiting_time.rb index 68fccb5c1..89a18a5d9 100644 --- a/app/models/vehicle_journey_control/waiting_time.rb +++ b/app/models/vehicle_journey_control/waiting_time.rb @@ -1,8 +1,9 @@ module VehicleJourneyControl class WaitingTime < ComplianceControl - hstore_accessor :control_attributes, maximum: :integer + store_accessor :control_attributes, :maximum - validates :maximum, numericality: true, allow_nil: true + validates_numericality_of :maximum, allow_nil: true, greater_than_or_equal_to: 0 + validates_presence_of :maximum def self.default_code; "3-VehicleJourney-1" end end diff --git a/app/models/workbench.rb b/app/models/workbench.rb index e36589210..b80fa64ac 100644 --- a/app/models/workbench.rb +++ b/app/models/workbench.rb @@ -4,6 +4,7 @@ class Workbench < ActiveRecord::Base belongs_to :line_referential belongs_to :stop_area_referential belongs_to :output, class_name: 'ReferentialSuite' + belongs_to :workgroup has_many :lines, -> (workbench) { Stif::MyWorkbenchScopes.new(workbench).line_scope(self) }, through: :line_referential has_many :networks, through: :line_referential @@ -14,6 +15,7 @@ class Workbench < ActiveRecord::Base has_many :workbench_imports has_many :compliance_check_sets has_many :compliance_control_sets + has_many :merges validates :name, presence: true validates :organisation, presence: true @@ -29,7 +31,12 @@ class Workbench < ActiveRecord::Base if line_ids.empty? Referential.none else - Referential.joins(:metadatas).where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]).ready + workgroup + .referentials + .joins(:metadatas) + .where(['referential_metadata.line_ids && ARRAY[?]::bigint[]', line_ids]) + .ready + .not_in_referential_suite end end diff --git a/app/models/workgroup.rb b/app/models/workgroup.rb new file mode 100644 index 000000000..3af20ae23 --- /dev/null +++ b/app/models/workgroup.rb @@ -0,0 +1,20 @@ +class Workgroup < ActiveRecord::Base + belongs_to :line_referential + belongs_to :stop_area_referential + + has_many :workbenches + has_many :calendars + has_many :organisations, through: :workbenches + has_many :referentials, through: :workbenches + + validates_uniqueness_of :name + + validates_presence_of :line_referential_id + validates_presence_of :stop_area_referential_id + + has_many :custom_fields + + def custom_fields_definitions + Hash[*custom_fields.map{|cf| [cf.code, cf]}.flatten] + end +end diff --git a/app/policies/access_link_policy.rb b/app/policies/access_link_policy.rb index 1f1147f60..f2ea7027f 100644 --- a/app/policies/access_link_policy.rb +++ b/app/policies/access_link_policy.rb @@ -6,14 +6,14 @@ class AccessLinkPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('access_links.create') + !referential_read_only? && organisation_match? && user.has_permission?('access_links.create') end def update? - !archived? && organisation_match? && user.has_permission?('access_links.update') + !referential_read_only? && organisation_match? && user.has_permission?('access_links.update') end def destroy? - !archived? && organisation_match? && user.has_permission?('access_links.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('access_links.destroy') end end diff --git a/app/policies/access_point_policy.rb b/app/policies/access_point_policy.rb index 41436e77c..4fa887b9e 100644 --- a/app/policies/access_point_policy.rb +++ b/app/policies/access_point_policy.rb @@ -6,14 +6,14 @@ class AccessPointPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('access_points.create') + !referential_read_only? && organisation_match? && user.has_permission?('access_points.create') end def update? - !archived? && organisation_match? && user.has_permission?('access_points.update') + !referential_read_only? && organisation_match? && user.has_permission?('access_points.update') end def destroy? - !archived? && organisation_match? && user.has_permission?('access_points.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('access_points.destroy') end end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index dbe4542e7..c44937c9e 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -81,6 +81,11 @@ class ApplicationPolicy @is_archived = is_archived end + def referential_read_only? + return @is_referential_read_only if instance_variable_defined?(:@is_referential_read_only) + @is_referential_read_only = is_referential_read_only + end + def organisation_match? user.organisation_id == organisation_id end @@ -124,4 +129,13 @@ class ApplicationPolicy current_referential.try(:archived_at) end end + + def is_referential_read_only + !!case referential + when Referential + referential.referential_read_only? + else + current_referential.try(:referential_read_only?) + end + end end diff --git a/app/policies/calendar_policy.rb b/app/policies/calendar_policy.rb index 074c41d8d..c2da8c924 100644 --- a/app/policies/calendar_policy.rb +++ b/app/policies/calendar_policy.rb @@ -5,18 +5,15 @@ class CalendarPolicy < ApplicationPolicy end end - def create? - !archived? && user.has_permission?('calendars.create') - end - def destroy? - !archived? & organisation_match? && user.has_permission?('calendars.destroy') - end - def update? - !archived? && organisation_match? && user.has_permission?('calendars.update') + def create? + user.has_permission?('calendars.create') end + def destroy?; instance_permission("destroy") end + def update?; instance_permission("update") end + def share?; instance_permission("share") end - def share? - user.organisation.name == 'STIF' # FIXME + private + def instance_permission permission + organisation_match? && user.has_permission?("calendars.#{permission}") end - end diff --git a/app/policies/connection_link_policy.rb b/app/policies/connection_link_policy.rb index 240c2a804..9bab5e4db 100644 --- a/app/policies/connection_link_policy.rb +++ b/app/policies/connection_link_policy.rb @@ -6,14 +6,14 @@ class ConnectionLinkPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('connection_links.create') + !referential_read_only? && organisation_match? && user.has_permission?('connection_links.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('connection_links.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('connection_links.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('connection_links.update') + !referential_read_only? && organisation_match? && user.has_permission?('connection_links.update') end end diff --git a/app/policies/journey_pattern_policy.rb b/app/policies/journey_pattern_policy.rb index 12bcced17..beb18d151 100644 --- a/app/policies/journey_pattern_policy.rb +++ b/app/policies/journey_pattern_policy.rb @@ -7,14 +7,14 @@ class JourneyPatternPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('journey_patterns.create') + !referential_read_only? && organisation_match? && user.has_permission?('journey_patterns.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('journey_patterns.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('journey_patterns.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('journey_patterns.update') + !referential_read_only? && organisation_match? && user.has_permission?('journey_patterns.update') end end diff --git a/app/policies/line_policy.rb b/app/policies/line_policy.rb index 67ea0b611..f7b03b0b5 100644 --- a/app/policies/line_policy.rb +++ b/app/policies/line_policy.rb @@ -6,7 +6,6 @@ class LinePolicy < ApplicationPolicy end def create? - Rails.logger.debug "LinePolicy.create?" user.has_permission?('lines.create') end @@ -14,20 +13,28 @@ class LinePolicy < ApplicationPolicy user.has_permission?('lines.destroy') end + def deactivate? + !record.deactivated? && user.has_permission?('lines.change_status') + end + + def activate? + record.deactivated? && user.has_permission?('lines.change_status') + end + def update? user.has_permission?('lines.update') end def create_footnote? - !archived? && organisation_match? && user.has_permission?('footnotes.create') + !referential_read_only? && organisation_match? && user.has_permission?('footnotes.create') end def edit_footnote? - !archived? && organisation_match? && user.has_permission?('footnotes.update') + !referential_read_only? && organisation_match? && user.has_permission?('footnotes.update') end def destroy_footnote? - !archived? && organisation_match? && user.has_permission?('footnotes.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('footnotes.destroy') end def update_footnote? ; edit_footnote? end diff --git a/app/policies/line_referential_policy.rb b/app/policies/line_referential_policy.rb new file mode 100644 index 000000000..ee742a083 --- /dev/null +++ b/app/policies/line_referential_policy.rb @@ -0,0 +1,14 @@ +class LineReferentialPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def synchronize?; instance_permission("synchronize") end + + private + def instance_permission permission + user.has_permission?("line_referentials.#{permission}") + end +end diff --git a/app/policies/merge_policy.rb b/app/policies/merge_policy.rb new file mode 100644 index 000000000..82eb72e08 --- /dev/null +++ b/app/policies/merge_policy.rb @@ -0,0 +1,15 @@ +class MergePolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def create? + user.has_permission?('merges.create') + end + + def update? + user.has_permission?('merges.update') + end +end diff --git a/app/policies/network_policy.rb b/app/policies/network_policy.rb index 9f86451a5..9b871d2b5 100644 --- a/app/policies/network_policy.rb +++ b/app/policies/network_policy.rb @@ -4,4 +4,15 @@ class NetworkPolicy < ApplicationPolicy scope end end + def create? + user.has_permission?('networks.create') + end + + def destroy? + user.has_permission?('networks.destroy') + end + + def update? + user.has_permission?('networks.update') + end end diff --git a/app/policies/purchase_window_policy.rb b/app/policies/purchase_window_policy.rb new file mode 100644 index 000000000..eb3b04bf7 --- /dev/null +++ b/app/policies/purchase_window_policy.rb @@ -0,0 +1,20 @@ +class PurchaseWindowPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def create? + !referential_read_only? && organisation_match? && user.has_permission?('purchase_windows.create') + end + + def update? + !referential_read_only? && organisation_match? && user.has_permission?('purchase_windows.update') + end + + def destroy? + !referential_read_only? && organisation_match? && user.has_permission?('purchase_windows.destroy') + end + +end diff --git a/app/policies/referential_policy.rb b/app/policies/referential_policy.rb index 253917509..f5c2d7c08 100644 --- a/app/policies/referential_policy.rb +++ b/app/policies/referential_policy.rb @@ -10,27 +10,27 @@ class ReferentialPolicy < ApplicationPolicy end def destroy? - !archived? && organisation_match? && user.has_permission?('referentials.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('referentials.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('referentials.update') + !referential_read_only? && organisation_match? && user.has_permission?('referentials.update') end def clone? - !archived? && create? + !record.in_referential_suite? && create? end def validate? - !archived? && create? && organisation_match? + !referential_read_only? && create? && organisation_match? end def archive? - record.archived_at.nil? && organisation_match? && user.has_permission?('referentials.update') + !referential_read_only? && record.archived_at.nil? && organisation_match? && user.has_permission?('referentials.update') end def unarchive? - !record.archived_at.nil? && organisation_match? && user.has_permission?('referentials.update') + record.archived? && !record.merged? && organisation_match? && user.has_permission?('referentials.update') end def common_lines? diff --git a/app/policies/route_policy.rb b/app/policies/route_policy.rb index 7e9fe251a..0337a5300 100644 --- a/app/policies/route_policy.rb +++ b/app/policies/route_policy.rb @@ -6,15 +6,15 @@ class RoutePolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('routes.create') + !referential_read_only? && organisation_match? && user.has_permission?('routes.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('routes.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('routes.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('routes.update') + !referential_read_only? && organisation_match? && user.has_permission?('routes.update') end def duplicate? diff --git a/app/policies/routing_constraint_zone_policy.rb b/app/policies/routing_constraint_zone_policy.rb index 3cfcf46ff..fd8081bef 100644 --- a/app/policies/routing_constraint_zone_policy.rb +++ b/app/policies/routing_constraint_zone_policy.rb @@ -6,14 +6,14 @@ class RoutingConstraintZonePolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('routing_constraint_zones.create') + !referential_read_only? && organisation_match? && user.has_permission?('routing_constraint_zones.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('routing_constraint_zones.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('routing_constraint_zones.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('routing_constraint_zones.update') + !referential_read_only? && organisation_match? && user.has_permission?('routing_constraint_zones.update') end end diff --git a/app/policies/stop_area_policy.rb b/app/policies/stop_area_policy.rb index faeebbc2a..fd73b7092 100644 --- a/app/policies/stop_area_policy.rb +++ b/app/policies/stop_area_policy.rb @@ -1,5 +1,13 @@ class StopAreaPolicy < ApplicationPolicy class Scope < Scope + def search_scope scope_name + scope = resolve + if scope_name&.to_s == "route_editor" + scope = scope.where("kind = ? OR area_type = ?", :non_commercial, 'zdep') unless user.organisation.has_feature?("route_stop_areas_all_types") + end + scope + end + def resolve scope end @@ -16,4 +24,12 @@ class StopAreaPolicy < ApplicationPolicy def update? user.has_permission?('stop_areas.update') end + + def deactivate? + !record.deactivated? && user.has_permission?('stop_areas.change_status') + end + + def activate? + record.deactivated? && user.has_permission?('stop_areas.change_status') + end end diff --git a/app/policies/stop_area_referential_policy.rb b/app/policies/stop_area_referential_policy.rb new file mode 100644 index 000000000..e370babf8 --- /dev/null +++ b/app/policies/stop_area_referential_policy.rb @@ -0,0 +1,14 @@ +class StopAreaReferentialPolicy < ApplicationPolicy + class Scope < Scope + def resolve + scope + end + end + + def synchronize?; instance_permission("synchronize") end + + private + def instance_permission permission + user.has_permission?("stop_area_referentials.#{permission}") + end +end diff --git a/app/policies/time_table_combination_policy.rb b/app/policies/time_table_combination_policy.rb index daa6808e4..bba458c18 100644 --- a/app/policies/time_table_combination_policy.rb +++ b/app/policies/time_table_combination_policy.rb @@ -7,6 +7,6 @@ class TimeTableCombinationPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('time_tables.update') + !referential_read_only? && organisation_match? && user.has_permission?('time_tables.update') end end diff --git a/app/policies/time_table_policy.rb b/app/policies/time_table_policy.rb index 92d3aef3e..390c170c7 100644 --- a/app/policies/time_table_policy.rb +++ b/app/policies/time_table_policy.rb @@ -7,23 +7,23 @@ class TimeTablePolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('time_tables.create') + !referential_read_only? && organisation_match? && user.has_permission?('time_tables.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('time_tables.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('time_tables.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('time_tables.update') + !referential_read_only? && organisation_match? && user.has_permission?('time_tables.update') end def actualize? - !archived? && organisation_match? && edit? + !referential_read_only? && organisation_match? && edit? end def duplicate? - !archived? && organisation_match? && create? + !referential_read_only? && organisation_match? && create? end def month? diff --git a/app/policies/vehicle_journey_policy.rb b/app/policies/vehicle_journey_policy.rb index 24040455f..adbc5fd89 100644 --- a/app/policies/vehicle_journey_policy.rb +++ b/app/policies/vehicle_journey_policy.rb @@ -6,14 +6,14 @@ class VehicleJourneyPolicy < ApplicationPolicy end def create? - !archived? && organisation_match? && user.has_permission?('vehicle_journeys.create') + !referential_read_only? && organisation_match? && user.has_permission?('vehicle_journeys.create') end def destroy? - !archived? && organisation_match? && user.has_permission?('vehicle_journeys.destroy') + !referential_read_only? && organisation_match? && user.has_permission?('vehicle_journeys.destroy') end def update? - !archived? && organisation_match? && user.has_permission?('vehicle_journeys.update') + !referential_read_only? && organisation_match? && user.has_permission?('vehicle_journeys.update') end end diff --git a/app/services/parent_import_notifier.rb b/app/services/parent_import_notifier.rb deleted file mode 100644 index 47e6755e4..000000000 --- a/app/services/parent_import_notifier.rb +++ /dev/null @@ -1,15 +0,0 @@ -class ParentImportNotifier - def self.notify_when_finished(imports = nil) - imports ||= imports_pending_notification - imports.each(&:notify_parent) - end - - def self.imports_pending_notification - Import - .where( - notified_parent_at: nil, - status: Import.finished_statuses - ) - .where.not(parent: nil) - end -end diff --git a/app/services/parent_notifier.rb b/app/services/parent_notifier.rb new file mode 100644 index 000000000..653c98aff --- /dev/null +++ b/app/services/parent_notifier.rb @@ -0,0 +1,19 @@ +class ParentNotifier + def initialize(klass) + @klass = klass + end + + def notify_when_finished(collection = nil) + collection ||= objects_pending_notification + collection.each(&:notify_parent) + end + + def objects_pending_notification + @klass + .where( + notified_parent_at: nil, + status: @klass.finished_statuses + ) + .where.not(parent: nil) + end +end diff --git a/app/services/referential_overview.rb b/app/services/referential_overview.rb new file mode 100644 index 000000000..ccfe0617a --- /dev/null +++ b/app/services/referential_overview.rb @@ -0,0 +1,256 @@ +class ReferentialOverview + attr_reader :h + attr_reader :referential + + PER_PAGE = 10 + + def initialize referential, h=nil + @referential = referential + @page = h && h.params[pagination_param_name]&.to_i || 1 + @h = h + end + + def lines + filtered_lines.includes(:company).map{|l| Line.new(l, @referential, period.first, h)} + end + + def period + @period ||= @referential.metadatas_period || [] + end + + def includes_today? + period.include? Time.now.to_date + end + + def weeks + @weeks = {} + period.map do |d| + @weeks[Week.key(d)] ||= Week.new(d, period.last, h) + end + @weeks.values + end + + def referential_lines + @referential.metadatas_lines + end + + def filtered_lines + search.result.page(@page).per_page(PER_PAGE) + end + + ### Pagination + + delegate :empty?, :first, :total_pages, :size, :total_entries, :offset, :length, to: :filtered_lines + def current_page + @page + end + + ### search + def search + lines = referential_lines + lines = lines.search h.params[search_param_name] + lines + end + + def pagination_param_name + "referential_#{@referential.slug}_overview" + end + + def search_param_name + "q_#{pagination_param_name}" + end + + class Line + attr_reader :h + attr_reader :referential_line + + delegate :name, :number, :company, :color, :transport_mode, to: :referential_line + + def initialize line, referential, start, h + @referential_line = line + @referential = referential + @start = start + @h = h + end + + def period + @period ||= @referential.metadatas_period || [] + end + + def referential_periods + @referential_periods ||= @referential.metadatas.include_lines([@referential_line.id]).map(&:periodes).flatten.sort{|p1, p2| p1.first <=> p2.first} + end + + def periods + @periods ||= begin + periods = referential_periods.flatten.map{|p| Period.new p, @start, h} + periods = fill_periods periods + periods = merge_periods periods + periods + end + end + + def fill_periods periods + [].tap do |out| + previous = OpenStruct.new(end: period.first - 1.day) + (periods + [OpenStruct.new(start: period.last + 1.day)]).each do |p| + if p.start > previous.end + 1.day + out << Period.new((previous.end+1.day..p.start-1.day), @start, h).tap{|p| p.empty = true} + end + out << p if p.respond_to?(:end) + previous = p + end + end + end + + def merge_periods periods + [].tap do |out| + current = periods.first + periods[1..-1].each do |p| + if p.start <= current.end + current.end = p.end + else + out << current + current = p + end + end + out << current + end + end + + def width + period.count * Day::WIDTH + end + + def html_style + { + width: "#{width}px" + }.map{|k, v| "#{k}: #{v}"}.join("; ") + end + + def html_class + out = [] + out + end + + class Period + attr_accessor :empty + attr_accessor :h + + def initialize period, start, h + @period = period + @start = start + @empty = false + @h = h + end + + def start + @period.first + end + + def end + @period.last + end + + def end= val + @period = (start..val) + end + + def width + @period.count * Day::WIDTH + end + + def left + (@period.first - @start).to_i * Day::WIDTH + end + + def html_style + { + width: "#{width}px", + left: "#{left}px", + }.map{|k, v| "#{k}: #{v}"}.join("; ") + end + + def empty? + @empty + end + + def accepted? + @period.count < 7 + end + + def title + h.l(self.start, format: :short) + " - " + h.l(self.end, format: :short) + end + + def html_class + out = [] + out << "empty" if empty? + out << "accepted" if accepted? + out + end + end + end + + class Week + attr_reader :h + attr_reader :start_date + attr_reader :end_date + + def initialize start_date, boundary, h + @start_date = start_date.to_date + @end_date = [start_date.end_of_week, boundary].min.to_date + @h = h + end + + def self.key date + date.beginning_of_week.to_s + end + + def span + h.l(@start_date, format: "#{@start_date.day}-#{@end_date.day} %b") + end + + def number + h.l(@start_date, format: "%W") + end + + def period + (@start_date..@end_date) + end + + def days + period.map {|d| Day.new d, h } + end + end + + class Day + attr_reader :h + + WIDTH=50 + + def initialize date, h + @date = date + @h = h + end + + def html_style + {width: "#{WIDTH}px"}.map{|k, v| "#{k}: #{v}"}.join("; ") + end + + def html_class + out = [h.l(@date, format: "%Y-%m-%d")] + out << "weekend" if [0, 6].include?(@date.wday) + out << "today" if @date == Time.now.to_date + out + end + + def short_name + h.l(@date, format: "%a") + end + + def number + @date.day + end + end +end diff --git a/app/services/zip_service.rb b/app/services/zip_service.rb index 7a4bdad1b..7166e6448 100644 --- a/app/services/zip_service.rb +++ b/app/services/zip_service.rb @@ -1,12 +1,16 @@ class ZipService - class Subdir < Struct.new(:name, :stream, :spurious) + class Subdir < Struct.new(:name, :stream, :spurious, :foreign_lines) + def ok? + foreign_lines.empty? && spurious.empty? + end end - attr_reader :current_key, :current_output, :current_spurious, :yielder + attr_reader :allowed_lines, :current_key, :foreign_lines, :current_output, :current_spurious, :yielder - def initialize data + def initialize data, allowed_lines @zip_data = StringIO.new(data) + @allowed_lines = allowed_lines @current_key = nil @current_output = nil end @@ -35,7 +39,8 @@ class ZipService end def add_to_current_output entry - return if is_spurious! entry.name + return if is_spurious!(entry.name) || is_foreign_line!(entry.name) + current_output.put_next_entry entry.name write_to_current_output entry.get_input_stream end @@ -52,20 +57,22 @@ class ZipService current_key, # Second part of the solution, yield the closed stream current_output.close_buffer, - current_spurious) + current_spurious.to_a, + foreign_lines) end end def open_new_output entry_key @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 = [] + @current_output = Zip::OutputStream.new(StringIO.new(''), true, nil) + @current_spurious = Set.new + @foreign_lines = [] end def entry_key entry # last dir name File.dirname.split("/").last - entry.name.split('/', -1)[-2] + entry.name.split('/').first end def is_spurious! entry_name @@ -75,4 +82,12 @@ class ZipService current_spurious << segments.second return true end + + def is_foreign_line! entry_name + STIF::NetexFile::Frame.get_short_id(entry_name).tap do | line_object_id | + return nil unless line_object_id + return nil if line_object_id.in? allowed_lines + foreign_lines << line_object_id + end + end end diff --git a/app/uploaders/import_uploader.rb b/app/uploaders/import_uploader.rb index 2740393ca..60e17ca0f 100644 --- a/app/uploaders/import_uploader.rb +++ b/app/uploaders/import_uploader.rb @@ -36,9 +36,9 @@ class ImportUploader < CarrierWave::Uploader::Base # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: - # def extension_whitelist - # %w(jpg jpeg gif png) - # end + def extension_whitelist + %w(zip) + end # Override the filename of the uploaded files: # Avoid using model.id or version_name here, see uploader/store.rb for details. diff --git a/app/views/api/v1/journey_patterns/show.rabl b/app/views/api/v1/journey_patterns/show.rabl index 3ea1bb6ff..aac66b6f3 100644 --- a/app/views/api/v1/journey_patterns/show.rabl +++ b/app/views/api/v1/journey_patterns/show.rabl @@ -1,20 +1,30 @@ object @journey_pattern extends "api/v1/trident_objects/show" -[:id, :name, :published_name, :registration_number, :comment].each do |attr| +[:id, :name, :published_name, :registration_number, :comment, :checksum].each do |attr| attributes attr, :unless => lambda { |m| m.send( attr).nil?} end +node :full_schedule do |journey_pattern| + journey_pattern.full_schedule? +end + +if has_feature? :costs_in_journey_patterns + attribute :costs +end + node(:route_short_description) do |journey_pattern| partial("api/v1/routes/short_description", :object => journey_pattern.route) end + node(:vehicle_journey_object_ids) do |journey_pattern| - journey_pattern.vehicle_journeys.map(&:objectid) + journey_pattern.vehicle_journeys.pluck(:objectid) end unless root_object.vehicle_journeys.empty? child :stop_points => :stop_area_short_descriptions do |stop_points| node do |stop_point| + cache stop_point.stop_area_id partial("api/v1/stop_areas/short_description", :object => stop_point.stop_area) end end diff --git a/app/views/autocomplete_purchase_windows/index.rabl b/app/views/autocomplete_purchase_windows/index.rabl new file mode 100644 index 000000000..1d0287602 --- /dev/null +++ b/app/views/autocomplete_purchase_windows/index.rabl @@ -0,0 +1,12 @@ +collection @purchase_windows, :object_root => false + +node do |window| + { + :id => window.id, + :name => window.name, + :objectid => window.objectid, + :color => window.color, + :short_id => window.get_objectid.short_id, + :text => "<strong><span class='fa fa-circle' style='color:" + (window.color ? window.color : '#4b4b4b') + "'></span> " + window.name + " - " + window.get_objectid.short_id + "</strong>" + } +end diff --git a/app/views/autocomplete_stop_areas/around.rabl b/app/views/autocomplete_stop_areas/around.rabl index bc8f06054..d067dc4d0 100644 --- a/app/views/autocomplete_stop_areas/around.rabl +++ b/app/views/autocomplete_stop_areas/around.rabl @@ -12,7 +12,7 @@ child @stop_areas, root: :features, object_root: false do name: s.name, short_name: truncate(s.name, :length => 30) || "", city_name: s.city_name, - area_type: s.area_type, + area_type: Chouette::AreaType.find(s.area_type).label, registration_number: s.registration_number, stoparea_id: s.id, text: "#{s.name}, #{s.zip_code} #{s.city_name}", diff --git a/app/views/autocomplete_stop_areas/index.rabl b/app/views/autocomplete_stop_areas/index.rabl index 5a9f76a47..c92b708f4 100644 --- a/app/views/autocomplete_stop_areas/index.rabl +++ b/app/views/autocomplete_stop_areas/index.rabl @@ -13,8 +13,9 @@ node do |stop_area| :user_objectid => stop_area.user_objectid, :longitude => stop_area.longitude, :latitude => stop_area.latitude, - :area_type => stop_area.area_type, - :comment => stop_area.comment + :area_type => Chouette::AreaType.find(stop_area.area_type).label, + :comment => stop_area.comment, + :text => stop_area.full_name } end diff --git a/app/views/calendar_mailer/created.html.slim b/app/views/calendar_mailer/created.html.slim index 37b2a86ea..bee071150 100644 --- a/app/views/calendar_mailer/created.html.slim +++ b/app/views/calendar_mailer/created.html.slim @@ -1,4 +1,4 @@ -div = t('mailers.calendar_mailer.created.body', cal_name: @calendar.name, cal_index_url: calendars_url) +div = t('mailers.calendar_mailer.created.body', cal_name: @calendar.name, cal_index_url: workgroup_calendars_url(@calendar.workgroup)) table style="border-collapse:collapse;font-family:'Open Sans', Arial, sans serif;width:550px;margin:0px auto;color:#333333;" @@ -16,7 +16,7 @@ table style="border-collapse:collapse;font-family:'Open Sans', Arial, sans serif = t('mailers.calendar_mailer.updated.subject') p style="font-size:14px;margin:0px 0px 10px 0px;" - = t('mailers.calendar_mailer.created.body', cal_name: @calendar.name, cal_index_url: calendars_url).html_safe + = t('mailers.calendar_mailer.created.body', cal_name: @calendar.name, cal_index_url: workgroup_calendars_url(@calendar.workgroup)).html_safe tr td style="text-align:center;padding:20px 0 0 0;border-top:1px solid #007fbb;" diff --git a/app/views/calendar_mailer/updated.html.slim b/app/views/calendar_mailer/updated.html.slim index bf128439a..0bdc2e7db 100644 --- a/app/views/calendar_mailer/updated.html.slim +++ b/app/views/calendar_mailer/updated.html.slim @@ -14,7 +14,7 @@ table style="border-collapse:collapse;font-family:'Open Sans', Arial, sans serif = t('mailers.calendar_mailer.updated.subject') p style="font-size:14px;margin:0px 0px 10px 0px;" - = t('mailers.calendar_mailer.updated.body', cal_name: @calendar.name, cal_index_url: calendars_url).html_safe + = t('mailers.calendar_mailer.updated.body', cal_name: @calendar.name, cal_index_url: workgroup_calendars_url(@calendar.workgroup)).html_safe tr td style="text-align:center;padding:20px 0 0 0;border-top:1px solid #007fbb;" diff --git a/app/views/calendars/_filters.html.slim b/app/views/calendars/_filters.html.slim index b5283c1e8..8bfe1974e 100644 --- a/app/views/calendars/_filters.html.slim +++ b/app/views/calendars/_filters.html.slim @@ -1,22 +1,22 @@ -= search_form_for @q, url: calendars_path, builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| += search_form_for @q, url: workgroup_calendars_path(@workgroup), builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_short_name_cont) = f.search_field :name_or_short_name_cont, class: 'form-control', placeholder: 'Indiquez un nom/nom court de calendrier...' span.input-group-btn button.btn.btn-default#search_btn type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :shared_true) = f.label Calendar.human_attribute_name(:shared), required: false, class: 'control-label' .form-group.checkbox_list = f.input :shared_true, as: :boolean, label: ("<span>Oui</span>").html_safe, wrapper_html: { class: 'checkbox-wrapper' } = f.input :shared_false, as: :boolean, label: ("<span>Non</span>").html_safe, wrapper_html: { class: 'checkbox-wrapper' } - .form-group + .form-group class=filter_item_class(params[:q], :contains_date) = f.label Calendar.human_attribute_name(:date), class: 'control-label' = f.input :contains_date, as: :date, label: false, wrapper_html: { class: 'date smart_date' }, class: 'form-control', include_blank: true .actions - = link_to 'Effacer', calendars_path, class: 'btn btn-link' + = link_to 'Effacer', workgroup_calendars_path(@workgroup), class: 'btn btn-link' = f.submit 'Filtrer', id: 'calendar_filter_btn', class: 'btn btn-default' diff --git a/app/views/calendars/_form.html.slim b/app/views/calendars/_form.html.slim deleted file mode 100644 index 3c152c61d..000000000 --- a/app/views/calendars/_form.html.slim +++ /dev/null @@ -1,53 +0,0 @@ -= simple_form_for @calendar, html: { class: 'form-horizontal', id: 'calendar_form' }, wrapper: :horizontal_form do |f| - .row - .col-lg-12 - = f.input :name - = f.input :short_name - - - if policy(@calendar).share? - .form-group.has_switch - = f.label :shared, class: 'col-sm-4 col-xs-5 control-label' - = f.input :shared, as: :boolean, checked_value: true, unchecked_value: false, label: content_tag(:span, t("#{@calendar.shared}"), class: 'switch-label', data: {checkedValue: t('true'), uncheckedValue: t('false')}), wrapper_html: { class: 'col-sm-8 col-xs-7'} - - .separator - - .row - .col-lg-12 - .subform - .nested-head - .wrapper - div - .form-group - label.control-label - = Calendar.human_attribute_name(:date) - div - - = f.simple_fields_for :date_values do |date_value| - = render 'date_value_fields', f: date_value - - .links.nested-linker - = link_to_add_association t('simple_form.labels.calendar.add_a_date'), f, :date_values, class: 'btn btn-outline-primary' - - .separator - - .row - .col-lg-12 - .subform - .nested-head - .wrapper - div - .form-group - label.control-label - = t('simple_form.labels.calendar.ranges.begin') - div - .form-group - label.control-label - = t('simple_form.labels.calendar.ranges.end') - div - - = f.simple_fields_for :periods do |period| - = render 'period_fields', f: period - .links.nested-linker - = link_to_add_association t('simple_form.labels.calendar.add_a_date_range'), f, :periods, class: 'btn btn-outline-primary' - - = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'calendar_form' diff --git a/app/views/calendars/_form_advanced.html.slim b/app/views/calendars/_form_advanced.html.slim new file mode 100644 index 000000000..e796e2e36 --- /dev/null +++ b/app/views/calendars/_form_advanced.html.slim @@ -0,0 +1,8 @@ +#periods + += javascript_tag do + | window.actionType = "#{raw params[:action]}"; + // | window.I18n = #{(I18n.backend.send(:translations)[I18n.locale].to_json).html_safe}; + | window.timetablesUrl = "#{calendar_url(@calendar).html_safe}"; + += javascript_pack_tag 'calendars/edit.js' diff --git a/app/views/calendars/_form_simple.html.slim b/app/views/calendars/_form_simple.html.slim new file mode 100644 index 000000000..ba18c765b --- /dev/null +++ b/app/views/calendars/_form_simple.html.slim @@ -0,0 +1,56 @@ +.row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = simple_form_for [@workgroup, @calendar], html: { class: 'form-horizontal', id: 'calendar_form' }, wrapper: :horizontal_form do |f| + .row + .col-lg-12 + = f.input :name + = f.input :short_name + + - if policy(@calendar).share? + .form-group.has_switch + = f.label :shared, class: 'col-sm-4 col-xs-5 control-label' + = f.input :shared, as: :boolean, checked_value: true, unchecked_value: false, label: content_tag(:span, t("#{@calendar.shared}"), class: 'switch-label', data: {checkedValue: t('true'), uncheckedValue: t('false')}), wrapper_html: { class: 'col-sm-8 col-xs-7'} + + .separator + + - unless has_feature?('application_days_on_calendars') + .row + .col-lg-12 + .subform + .nested-head + .wrapper + div + .form-group + label.control-label + = Calendar.human_attribute_name(:date) + div + + = f.simple_fields_for :date_values do |date_value| + = render 'date_value_fields', f: date_value + + .links.nested-linker + = link_to_add_association t('simple_form.labels.calendar.add_a_date'), f, :date_values, class: 'btn btn-outline-primary' + + .separator + + .row + .col-lg-12 + .subform + .nested-head + .wrapper + div + .form-group + label.control-label + = t('simple_form.labels.calendar.ranges.begin') + div + .form-group + label.control-label + = t('simple_form.labels.calendar.ranges.end') + div + + = f.simple_fields_for :periods do |period| + = render 'period_fields', f: period + .links.nested-linker + = link_to_add_association t('simple_form.labels.calendar.add_a_date_range'), f, :periods, class: 'btn btn-outline-primary' + + = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'calendar_form' diff --git a/app/views/calendars/edit.html.slim b/app/views/calendars/edit.html.slim index e806fc94b..79ab1f5d0 100644 --- a/app/views/calendars/edit.html.slim +++ b/app/views/calendars/edit.html.slim @@ -1,7 +1,8 @@ -- breadcrumb :calendar, @calendar +- breadcrumb :calendar, @workgroup, @calendar - page_header_content_for @calendar .page_content .container-fluid - .row - .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 - = render 'form' + - if has_feature?('application_days_on_calendars') + = render 'form_advanced' + - else + = render 'form_simple' diff --git a/app/views/calendars/index.html.slim b/app/views/calendars/index.html.slim index 77478a624..0b58c0c72 100644 --- a/app/views/calendars/index.html.slim +++ b/app/views/calendars/index.html.slim @@ -1,7 +1,4 @@ -- breadcrumb :calendars -- content_for :page_header_actions do - - if policy(Calendar).create? - = link_to(t('actions.add'), new_calendar_path, class: 'btn btn-default') +- breadcrumb :calendars, workgroup .page_content .container-fluid @@ -19,7 +16,7 @@ key: :name, \ attribute: 'name', \ link_to: lambda do |calendar| \ - calendar_path(calendar) \ + workgroup_calendar_path(workgroup, calendar) \ end \ ), \ TableBuilderHelper::Column.new( \ @@ -35,7 +32,6 @@ attribute: Proc.new { |c| t("#{c.try(:shared)}") } \ ) \ ], - links: [:show, :edit], cls: 'table has-filter' = new_pagination @calendars, 'pull-right' diff --git a/app/views/calendars/month.rabl b/app/views/calendars/month.rabl new file mode 100644 index 000000000..1584db44c --- /dev/null +++ b/app/views/calendars/month.rabl @@ -0,0 +1,9 @@ +object @calendar + +node do |tt| + { + name: I18n.l(@date, format: '%B'), + days: tt.month_inspect(@date), + day_types: %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| tt.send(d) }.map{ |d| tt.human_attribute_name(d).first(2)}.join('') + } +end diff --git a/app/views/calendars/new.html.slim b/app/views/calendars/new.html.slim index ce8b6a036..5657a0c55 100644 --- a/app/views/calendars/new.html.slim +++ b/app/views/calendars/new.html.slim @@ -1,6 +1,4 @@ -- breadcrumb :calendars +- breadcrumb :calendars, @workgroup .page_content .container-fluid - .row - .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 - = render 'form' + = render 'form_simple' diff --git a/app/views/calendars/show.html.slim b/app/views/calendars/show.html.slim index da4afa3e6..cec4f66a5 100644 --- a/app/views/calendars/show.html.slim +++ b/app/views/calendars/show.html.slim @@ -1,25 +1,24 @@ -- breadcrumb :calendar, @calendar +- breadcrumb :calendar, @workgroup, @calendar - page_header_content_for @calendar -- content_for :page_header_content do - .row.mb-sm - .col-lg-12.text-right - - @calendar.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content -- if policy(@calendar).edit? - - content_for :page_header_actions do - = link_to(t('actions.edit'), edit_calendar_path(@calendar), class: 'btn btn-default') .page_content .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 = definition_list t('metadatas'), - { 'Nom court' => @calendar.try(:short_name), - Calendar.human_attribute_name(:shared) => t("#{@calendar.shared}"), - 'Organisation' => @calendar.organisation.name, - Calendar.human_attribute_name(:dates) => @calendar.dates.collect{|d| l(d, format: :short)}.join(', ').html_safe, - Calendar.human_attribute_name(:date_ranges) => @calendar.periods.map{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe } + { 'Nom court' => resource.try(:short_name), + Calendar.human_attribute_name(:shared) => t("#{resource.shared}"), + 'Organisation' => resource.organisation.name, + Calendar.human_attribute_name(:dates) => resource.dates.collect{|d| l(d, format: :short)}.join(', ').html_safe, + Calendar.human_attribute_name(:date_ranges) => resource.periods.map{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe } + + - if has_feature?('application_days_on_calendars') + .row + .col-lg-12.mb-sm + .pagination.pull-right + = @year + .page_links + = link_to '', calendar_path(@calendar, year: (@year - 1)), class: 'previous_page' + = link_to '', calendar_path(@calendar, year: (@year + 1)), class: 'next_page' + + = render 'time_tables/show_time_table', time_table: @calendar diff --git a/app/views/calendars/show.rabl b/app/views/calendars/show.rabl new file mode 100644 index 000000000..295d97528 --- /dev/null +++ b/app/views/calendars/show.rabl @@ -0,0 +1,22 @@ +object @calendar + +attributes :id +node do |tt| + { + comment: tt.name, + time_table_bounding: tt.presenter.time_table_bounding, + day_types: %w(monday tuesday wednesday thursday friday saturday sunday).select{ |d| tt.send(d) }.map{ |d| tt.human_attribute_name(d).first(2)}.join(''), + current_month: tt.month_inspect(Date.today), + periode_range: month_periode_enum(3), + current_periode_range: Date.today.beginning_of_month, + short_id: tt.object_id, + } +end + +child(:periods, object_root: false, root: :time_table_periods) do + attributes :id, :period_start, :period_end +end + +child(:all_dates, object_root: false, root: :time_table_dates) do + attributes :id, :date, :in_out +end diff --git a/app/views/companies/index.html.slim b/app/views/companies/index.html.slim index 5d746642f..9f1502e54 100644 --- a/app/views/companies/index.html.slim +++ b/app/views/companies/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :companies, @line_referential -- content_for :page_header_actions do - - if policy(Chouette::Company).create? - = link_to(t('companies.actions.new'), new_line_referential_company_path(@line_referential), class: 'btn btn-primary') .page_content .container-fluid @@ -34,7 +31,6 @@ end \ ) \ ], - links: [:show, :edit], cls: 'table has-search' = new_pagination @companies, 'pull-right' diff --git a/app/views/companies/show.html.slim b/app/views/companies/show.html.slim index 0d6b4aae3..ca0a410b3 100644 --- a/app/views/companies/show.html.slim +++ b/app/views/companies/show.html.slim @@ -1,25 +1,13 @@ - breadcrumb :company, @company -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - if policy(Chouette::Company).create? - = link_to t('companies.actions.new'), new_line_referential_company_path(@line_referential), class: 'btn btn-primary' - - if policy(@company).update? - = link_to t('companies.actions.edit'), edit_line_referential_company_path(@line_referential, @company), class: 'btn btn-primary' - - if policy(@company).destroy? - = link_to line_referential_company_path(@line_referential, @company), method: :delete, data: {confirm: t('companies.actions.destroy_confirm')}, class: 'btn btn-primary' do - span.fa.fa-trash - span = t('companies.actions.destroy') - page_header_content_for @company - .page_content .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 = definition_list t('metadatas'), { 'ID Codif' => @company.try(:get_objectid).try(:short_id), - Chouette::Company.human_attribute_name(:phone) => @company.phone, - Chouette::Company.human_attribute_name(:email) => @company.email, - Chouette::Company.human_attribute_name(:url) => @company.url } + Chouette::Company.human_attribute_name(:phone) => resource.phone, + Chouette::Company.human_attribute_name(:email) => resource.email, + Chouette::Company.human_attribute_name(:url) => resource.url } diff --git a/app/views/compliance_check_sets/_filters.html.slim b/app/views/compliance_check_sets/_filters.html.slim index bf929bc08..e413a6cdd 100644 --- a/app/views/compliance_check_sets/_filters.html.slim +++ b/app/views/compliance_check_sets/_filters.html.slim @@ -1,23 +1,23 @@ = search_form_for @q_for_form, url: workbench_compliance_check_sets_path(@workbench), builder: SimpleForm::FormBuilder, class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :referential_name_cont) = f.search_field :referential_name_cont, class: 'form-control', placeholder: t('compliance_check_sets.filters.name') span.input-group-btn button.btn.btn-default type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :parent_type_eq_any) = f.label t('activerecord.attributes.compliance_check_set.assigned_to'), required: false, class: 'control-label' = f.input :parent_type_eq_any, collection: ComplianceCheckSet.order('parent_type'), as: :check_boxes, label: false, label_method: lambda {|w| ("<span>#{w}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :created_at) = f.label Import.human_attribute_name(:created_at), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :created_at do |p| = p.input :start_date, as: :date, label: false, wrapper_html: {class: 'date smart_date filter_menu-item'}, default: @begin_range, include_blank: @begin_range ? false : true = p.input :end_date, as: :date, label: false, wrapper_html: {class: 'date smart_date filter_menu-item'}, default: @end_range, include_blank: @end_range ? false : true .form-group.search - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :compliance_control_set_name_cont) = f.search_field :compliance_control_set_name_cont, class: 'form-control', placeholder: t('compliance_check_sets.filters.name_compliance_control_set') span.input-group-btn button.btn.btn-default type='submit' diff --git a/app/views/compliance_check_sets/executed.html.slim b/app/views/compliance_check_sets/executed.html.slim index da8a2a3de..b33b516b4 100644 --- a/app/views/compliance_check_sets/executed.html.slim +++ b/app/views/compliance_check_sets/executed.html.slim @@ -1,4 +1,4 @@ -- breadcrumb :compliance_check_set, @workbench, @compliance_check_set +- breadcrumb :compliance_check_set_executed, @workbench, @compliance_check_set - page_header_content_for @compliance_check_set .page_content diff --git a/app/views/compliance_check_sets/index.html.slim b/app/views/compliance_check_sets/index.html.slim index f5d1bd777..31ad31e5b 100644 --- a/app/views/compliance_check_sets/index.html.slim +++ b/app/views/compliance_check_sets/index.html.slim @@ -12,7 +12,10 @@ [ \ TableBuilderHelper::Column.new( \ key: :ref, \ - attribute: 'compliance_check_set_id' \ + attribute: 'id', \ + link_to: lambda do |compliance_check_set| \ + workbench_compliance_check_set_path(compliance_check_set.workbench_id, compliance_check_set.id) \ + end \ ), \ TableBuilderHelper::Column.new( \ key: :creation_date, \ @@ -20,9 +23,9 @@ ), \ TableBuilderHelper::Column.new( \ key: :associated_object, \ - attribute: Proc.new{|n| n.referential.name}, \ + attribute: Proc.new{|n| n.referential.present? ? n.referential.name : ''}, \ link_to: lambda do |compliance_check_set| \ - referential_path(compliance_check_set.referential_id) \ + compliance_check_set.referential.present? ? referential_path(compliance_check_set.referential_id) : '#' \ end \ ), \ TableBuilderHelper::Column.new( \ @@ -31,15 +34,15 @@ ), \ TableBuilderHelper::Column.new(\ key: :compliance_control_set, \ - attribute: Proc.new{ |n| (n.compliance_control_set.name) if n.compliance_control_set} \ + attribute: 'name', \ + link_to: lambda do |compliance_check_set| \ + executed_workbench_compliance_check_set_path(compliance_check_set.workbench_id, compliance_check_set.id) \ + end \ ), \ ], sortable: true, - links: [:show], cls: 'table has-filter has-search' - unless @compliance_check_sets.any? .row.mt-xs .col-lg-12 = replacement_msg t('compliance_check_sets.search_no_results') - - diff --git a/app/views/compliance_check_sets/show.html.slim b/app/views/compliance_check_sets/show.html.slim index 5d8e3fa15..4df14ab06 100644 --- a/app/views/compliance_check_sets/show.html.slim +++ b/app/views/compliance_check_sets/show.html.slim @@ -1,41 +1,27 @@ - breadcrumb :compliance_check_sets, @workbench, @compliance_check_set -/ PageHeader -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @compliance_check_set.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content - - page_header_content_for @compliance_check_set -/ = pageheader 'jeux-de-donnees', -/ @compliance_check_set.name, -/ '', -/ t('last_update', time: l(@compliance_check_set.updated_at, format: :short)) do - - / Below is secundary actions & optional contents (filters, ...) - / .row - / .col-lg-12.text-right.mb-sm - / - @compliance_check_set.action_links.each do |link| - / = link_to link.href, - / method: link.method, - / data: link.data, - / class: 'btn btn-primary' do - / = link.content / PageContent .page_content.import_messages .container-fluid .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list( t('metadatas'), + { I18n.t("compliance_check_sets.show.metadatas.referential") => (@compliance_check_set.referential.nil? ? '' : link_to(@compliance_check_set.referential.name, referential_path(@compliance_check_set.referential)) ), + I18n.t("compliance_check_sets.show.metadatas.referential_type") => 'Jeu de données', + I18n.t("compliance_check_sets.show.metadatas.compliance_check_set_executed") => link_to(@compliance_check_set.name, executed_workbench_compliance_check_set_path(@compliance_check_set.workbench_id, @compliance_check_set)), + I18n.t("compliance_check_sets.show.metadatas.compliance_control_owner") => @compliance_check_set.organisation.name, + I18n.t("compliance_check_sets.show.metadatas.import") => '' }) + .row .col-lg-12 h1 span.status_icon = compliance_check_set_status(@compliance_check_set.status) - span = t('compliance_check_sets.show.table_state', lines_status: @compliance_check_set.lines_status , lines_in_compliance_check_set: @compliance_check_set.lines_in_compliance_check_set ) + span = t('.table_state', lines_status: @compliance_check_set.lines_status , lines_in_compliance_check_set: @compliance_check_set.lines_in_compliance_check_set ) + .col-lg-12 + h2 = t('.table_title') .col-lg-12 - = t('compliance_check_sets.show.table_explanation') + = t('.table_explanation') + .row .col-lg-12 = table_builder_2 @compliance_check_set.compliance_check_resources, @@ -46,13 +32,21 @@ ), \ TableBuilderHelper::Column.new( \ key: :status, \ - attribute: Proc.new { |n| compliance_check_set_status(n.status) } \ + attribute: Proc.new { |n| compliance_check_resource_status(n.status) } \ ), \ TableBuilderHelper::Column.new( \ key: :metrics, \ attribute: Proc.new { |n| I18n.t('compliance_check_sets.show.metrics', n.metrics.deep_symbolize_keys) } \ + ), \ + TableBuilderHelper::Column.new( \ + name: 'Téléchargement' , \ + attribute: Proc.new { |n| '<i class="fa fa-download" aria-hidden="true"></i>'.html_safe }, \ + sortable: false, \ + link_to: lambda do |compliance_check_resource| \ + workbench_compliance_check_set_compliance_check_messages_path(@compliance_check_set.workbench, @compliance_check_set, format: 'csv', compliance_check_resource_id: compliance_check_resource.id ) \ + end \ ) \ ], sortable: false, \ links: [], - cls: 'table' + cls: 'table has-search' diff --git a/app/views/compliance_checks/_filters.html.slim b/app/views/compliance_checks/_filters.html.slim index 0d747da27..40e45dd92 100644 --- a/app/views/compliance_checks/_filters.html.slim +++ b/app/views/compliance_checks/_filters.html.slim @@ -5,7 +5,7 @@ class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_cont) = f.search_field :name_cont, class: 'form-control', placeholder: t('compliance_checks.filters.name') @@ -14,7 +14,7 @@ span.fa.fa-search .ffg-row - .form-group.togglable#compliance_check_block-filter + .form-group.togglable#compliance_check_block-filter class=filter_item_class(params[:q], :compliance_check_block_id_eq_any) = f.label t('activerecord.models.compliance_check_block.one'), required: false, class: 'control-label' = f.input :compliance_check_block_id_eq_any, collection: @compliance_check_set.compliance_check_blocks, @@ -23,7 +23,7 @@ label_method: lambda {|w| ("<span>#{transport_mode_text(w)}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - .form-group.togglable#subclass-filter + .form-group.togglable#subclass-filter class=filter_item_class(params[:q], :origin_code_cont_any) = f.label t('compliance_checks.filters.subclass'), required: false, class: 'control-label' = f.input :origin_code_cont_any, collection: subclass_selection_list, @@ -32,7 +32,7 @@ label_method: lambda {|w| ("<span>#{w.first}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - .form-group.togglable#severity-filter + .form-group.togglable#severity-filter class=filter_item_class(params[:q], :criticity_eq_any) = f.label t('compliance_checks.filters.criticity'), required: false, class: 'control-label' = f.input :criticity_eq_any, collection: ComplianceControl.criticities, diff --git a/app/views/compliance_control_sets/_filters.html.slim b/app/views/compliance_control_sets/_filters.html.slim index 6a5d3ac44..4348defac 100644 --- a/app/views/compliance_control_sets/_filters.html.slim +++ b/app/views/compliance_control_sets/_filters.html.slim @@ -1,16 +1,16 @@ = search_form_for @q_for_form, url: compliance_control_sets_path, builder: SimpleForm::FormBuilder, class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_cont) = f.search_field :name_cont, class: 'form-control', placeholder: t('compliance_control_sets.filters.name') span.input-group-btn button.btn.btn-default type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :organisation_name_eq_any) = f.label t('activerecord.models.organisation.one'), required: false, class: 'control-label' = f.input :organisation_name_eq_any, collection: organisations_filters_values, as: :check_boxes, label: false, label_method: lambda {|w| ("<span>#{w.name}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - - .form-group.togglable + + .form-group.togglable class=filter_item_class(params[:q], :updated_at) = f.label Import.human_attribute_name(:updated_at), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :updated_at do |p| @@ -19,4 +19,4 @@ .actions = link_to t('actions.erase'), @compliance_control_set, class: 'btn btn-link' - = f.submit t('actions.filter'), class: 'btn btn-default', id: 'compliance_control_set_filter_btn'
\ No newline at end of file + = f.submit t('actions.filter'), class: 'btn btn-default', id: 'compliance_control_set_filter_btn' diff --git a/app/views/compliance_control_sets/index.html.slim b/app/views/compliance_control_sets/index.html.slim index 2a5651280..144a4e5b9 100644 --- a/app/views/compliance_control_sets/index.html.slim +++ b/app/views/compliance_control_sets/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :compliance_control_sets -- content_for :page_header_actions do - - if policy(ComplianceControlSet).create? - = link_to(t('compliance_control_sets.actions.new'), new_compliance_control_set_path, class: 'btn btn-default') .page_content .container-fluid @@ -30,7 +27,7 @@ ), \ TableBuilderHelper::Column.new( \ key: :control_numbers, \ - attribute: 'control_numbers' \ + attribute: Proc.new {|n| n.compliance_controls.count }\ ), \ TableBuilderHelper::Column.new( \ key: :updated_at, \ @@ -38,7 +35,6 @@ ) \ ], sortable: true, - links: [:show], cls: 'table has-filter has-search' = new_pagination @compliance_control_sets, 'pull-right' diff --git a/app/views/compliance_control_sets/show.html.slim b/app/views/compliance_control_sets/show.html.slim index d915bbdaf..59100681d 100644 --- a/app/views/compliance_control_sets/show.html.slim +++ b/app/views/compliance_control_sets/show.html.slim @@ -1,17 +1,5 @@ - breadcrumb :compliance_control_set, @compliance_control_set - page_header_content_for @compliance_control_set -- content_for :page_header_content do - .row.mb-sm - .col-lg-12.text-right - - @compliance_control_set.action_links.each do |link| - - if link.is_a?(HTMLElement) - = link.to_html(class: 'btn btn-primary') - - else - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content .page_content .container-fluid diff --git a/app/views/compliance_controls/_filters.html.slim b/app/views/compliance_controls/_filters.html.slim index d38da5d2d..f6b9970f2 100644 --- a/app/views/compliance_controls/_filters.html.slim +++ b/app/views/compliance_controls/_filters.html.slim @@ -5,7 +5,7 @@ class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_cont) = f.search_field :name_cont, class: 'form-control', placeholder: t('compliance_controls.filters.name') @@ -14,7 +14,7 @@ span.fa.fa-search .ffg-row - .form-group.togglable#compliance_control_block-filter + .form-group.togglable#compliance_control_block-filter class=filter_item_class(params[:q], :compliance_control_block_id_eq_any) = f.label t('activerecord.models.compliance_control_block.one'), required: false, class: 'control-label' = f.input :compliance_control_block_id_eq_any, collection: @compliance_control_set.compliance_control_blocks, @@ -23,7 +23,7 @@ label_method: lambda {|w| ("<span>#{transport_mode_text(w)}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - .form-group.togglable#subclass-filter + .form-group.togglable#subclass-filter class=filter_item_class(params[:q], :origin_code_cont_any) = f.label t('compliance_controls.filters.subclass'), required: false, class: 'control-label' = f.input :origin_code_cont_any, collection: subclass_selection_list, @@ -32,7 +32,7 @@ label_method: lambda {|w| ("<span>#{w.first}</span>").html_safe}, required: false, wrapper_html: {class: 'checkbox_list'} - .form-group.togglable#severity-filter + .form-group.togglable#severity-filter class=filter_item_class(params[:q], :criticity_eq_any) = f.label t('compliance_controls.filters.criticity'), required: false, class: 'control-label' = f.input :criticity_eq_any, collection: ComplianceControl.criticities, diff --git a/app/views/compliance_controls/new.html.slim b/app/views/compliance_controls/new.html.slim index f7f47fba3..c0abc522f 100644 --- a/app/views/compliance_controls/new.html.slim +++ b/app/views/compliance_controls/new.html.slim @@ -6,5 +6,5 @@ = render 'form' = definition_list t('metadatas'), - I18n.t('activerecord.attributes.compliance_control.predicate') => @compliance_control.class.predicate, - I18n.t('activerecord.attributes.compliance_control.prerequisite') => @compliance_control.class.prerequisite + I18n.t('activerecord.attributes.compliance_control.predicate') => @compliance_control.predicate, + I18n.t('activerecord.attributes.compliance_control.prerequisite') => @compliance_control.prerequisite diff --git a/app/views/compliance_controls/show.html.slim b/app/views/compliance_controls/show.html.slim index 54b07abf1..ab25747a9 100644 --- a/app/views/compliance_controls/show.html.slim +++ b/app/views/compliance_controls/show.html.slim @@ -1,6 +1,4 @@ - breadcrumb :compliance_control, @compliance_control -- content_for :page_header_actions do - = link_to(t('actions.edit'), edit_compliance_control_set_compliance_control_path(params[:compliance_control_set_id], params[:id]), class: 'btn btn-default') - page_header_content_for @compliance_control @@ -15,8 +13,8 @@ ComplianceControl.human_attribute_name(:code) => @compliance_control.code, ComplianceControl.human_attribute_name(:criticity) => @compliance_control.criticity, ComplianceControl.human_attribute_name(:comment) => @compliance_control.comment, - I18n.t('activerecord.attributes.compliance_control.predicate') => @compliance_control.class.predicate, - I18n.t('activerecord.attributes.compliance_control.prerequisite') => @compliance_control.class.prerequisite, + I18n.t('activerecord.attributes.compliance_control.predicate') => @compliance_control.predicate, + I18n.t('activerecord.attributes.compliance_control.prerequisite') => @compliance_control.prerequisite, }.merge( \ {}.tap do |hash| \ @compliance_control.class.dynamic_attributes.each do |attribute| \ diff --git a/app/views/dashboards/_dashboard.html.slim b/app/views/dashboards/_dashboard.html.slim index f03301e23..05257a766 100644 --- a/app/views/dashboards/_dashboard.html.slim +++ b/app/views/dashboards/_dashboard.html.slim @@ -5,11 +5,11 @@ .panel-heading h3.panel-title.with_actions div - = workbench.name + = link_to workbench.name, workbench_path(workbench) span.badge.ml-xs = workbench.referentials.count if workbench.referentials.present? div - = link_to '', workbench_path(workbench), class: ' fa fa-chevron-right pull-right', title: t('.offers.see') + = link_to '', workbench_path(workbench), class: ' fa fa-chevron-right pull-right', title: t('workbenches.index.offers.see') - if workbench.referentials.present? .list-group @@ -17,35 +17,38 @@ = link_to referential.name, referential_path(referential, workbench_id: referential.workbench_id, current_workbench_id: workbench.id), class: 'list-group-item' - else .panel-body - em.small.text-muted = t('.offers.no_content') + em.small.text-muted = t('workbenches.index.offers.no_content') .panel.panel-default .panel-heading h3.panel-title.with_actions - = "Modèles de calendrier" + = link_to I18n.t("activerecord.models.calendar", count: @dashboard.current_organisation.calendars.size), workgroup_calendars_path(current_workgroup) div - = link_to '', calendars_path, class: ' fa fa-chevron-right pull-right' + = link_to '', workgroup_calendars_path(current_workgroup), class: ' fa fa-chevron-right pull-right' - if @dashboard.current_organisation.calendars.present? .list-group - @dashboard.current_organisation.calendars.order("updated_at desc").limit(5).each do |calendar| - = link_to calendar.name, calendar_path(calendar), class: 'list-group-item' + = link_to calendar.name, workgroup_calendars_path(current_workgroup, calendar), class: 'list-group-item' - else .panel-body - em.small.text-muted Aucun modèle de calendrier défini + em.small.text-muted + = t('dasboard.calendars.none') .col-lg-6.col-md-6.col-sm-6.col-xs-12 .panel.panel-default - .panel-heading - h3.panel-title - = "Référentiels d'arrêts" - .list-group - - @dashboard.current_organisation.stop_area_referentials.each do |referential| - = link_to referential.name, stop_area_referential_stop_areas_path(referential), class: 'list-group-item' + - @dashboard.current_organisation.stop_area_referentials.each do |referential| + .panel-heading + h3.panel-title + = referential.name + .list-group + = link_to Chouette::StopArea.model_name.human.pluralize.capitalize, stop_area_referential_stop_areas_path(referential), class: 'list-group-item' .panel.panel-default - .panel-heading - h3.panel-title - = "Référentiels de lignes" - .list-group - - @dashboard.current_organisation.line_referentials.all.each do |referential| - = link_to referential.name, line_referential_lines_path(referential), class: 'list-group-item' + - @dashboard.current_organisation.line_referentials.all.each do |referential| + .panel-heading + h3.panel-title + = referential.name + .list-group + = link_to Chouette::Line.model_name.human.pluralize.capitalize, line_referential_lines_path(referential), class: 'list-group-item' + = link_to Chouette::Company.model_name.human.pluralize.capitalize, line_referential_companies_path(referential), class: 'list-group-item' + = link_to Chouette::Network.model_name.human.pluralize.capitalize, line_referential_networks_path(referential), class: 'list-group-item' diff --git a/app/views/devise/invitations/edit.html.slim b/app/views/devise/invitations/edit.html.slim index 6c9a6f436..7a22146c0 100644 --- a/app/views/devise/invitations/edit.html.slim +++ b/app/views/devise/invitations/edit.html.slim @@ -1,14 +1,19 @@ -.col-md-offset-2.col-md-8 - .panel.panel-default - .panel-heading = t('devise.invitations.edit.header') - - .panel-body - = simple_form_for resource, as: resource_name, :url => invitation_path(resource_name), :html => { :method => :put, class: "form-horizontal" } do |form| - = form.hidden_field :invitation_token - - = form.input :name - = form.input :password, as: :password - = form.input :password_confirmation, as: :password - - .submit - = form.button :submit, value: t('devise.invitations.edit.submit_button'), class: 'btn-info'
\ No newline at end of file +/ PageHeader + +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = simple_form_for resource, as: resource_name, :url => invitation_path(resource_name), :html => { :method => :put, class: "form-horizontal", id: 'invitation_form' } do |form| + .row + .col-lg-12 + = form.hidden_field :invitation_token + + = form.input :name + = form.input :password, as: :password + = form.input :password_confirmation, as: :password + + = form.button :submit, value: t('devise.invitations.edit.submit_button'), class: 'btn-info btn-default formSubmitr', form: 'invitation_form' diff --git a/app/views/devise/mailer/invitation_instructions.fr.html.slim b/app/views/devise/mailer/invitation_instructions.fr.html.slim index 484e0d68d..e263e188a 100644 --- a/app/views/devise/mailer/invitation_instructions.fr.html.slim +++ b/app/views/devise/mailer/invitation_instructions.fr.html.slim @@ -1,7 +1,7 @@ -p = t("devise.mailer.invitation_instructions.hello", email: @resource.email) +p = t("devise.mailer.invitation_instructions.hello", email: @resource.name) p = t("devise.mailer.invitation_instructions.someone_invited_you", url: unauthenticated_root_url) p = link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token) -p = t("devise.mailer.invitation_instructions.ignore").html_safe
\ No newline at end of file +p = t("devise.mailer.invitation_instructions.ignore").html_safe diff --git a/app/views/devise/passwords/edit.html.slim b/app/views/devise/passwords/edit.html.slim index 864a44499..0d18f657c 100644 --- a/app/views/devise/passwords/edit.html.slim +++ b/app/views/devise/passwords/edit.html.slim @@ -1,13 +1,17 @@ -.col-md-offset-2.col-md-8 - .panel.panel-default - .panel-heading = t('.title') - .panel-body - = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, class: "form-horizontal" }) do |f| +/ PageHeader - = f.input :reset_password_token, as: :hidden - = f.input :password, as: :password - = f.input :password_confirmation, as: :password - - .form-actions - = link_to t("cancel"), unauthenticated_root_path, class: 'btn btn-default' - = f.button :submit, :value => t("devise.passwords.edit.commit"), class: 'btn-info'
\ No newline at end of file +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, class: "form-horizontal", id: 'password_form' }) do |f| + .row + .col-lg-12 + = f.input :reset_password_token, as: :hidden + = f.input :password, as: :password + = f.input :password_confirmation, as: :password + + = f.button :submit, :value => t("devise.passwords.edit.commit"), class: 'btn btn-default formSubmitr', form: 'password_form' diff --git a/app/views/devise/passwords/new.html.slim b/app/views/devise/passwords/new.html.slim index 8a739ecc1..303f78f0e 100644 --- a/app/views/devise/passwords/new.html.slim +++ b/app/views/devise/passwords/new.html.slim @@ -1,11 +1,15 @@ -.col-md-offset-2.col-md-8 - .panel.panel-default - .panel-heading = t('.title') - - .panel-body - = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal' } ) do |form| - = form.input :email, :as => :email, placeholder: 'user@domain.com' +/ PageHeader - .form-actions - = link_to t("cancel"), unauthenticated_root_path, class: 'btn btn-default' - = form.button :submit, :value => t("devise.passwords.new.commit"), class: 'btn-info'
\ No newline at end of file +- content_for :page_header_title, t('.title') + +/ PageContent +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = simple_form_for(resource, :as => resource_name, :url => password_path(resource_name), html: {class: 'form-horizontal', id: 'password_form' } ) do |form| + .row + .col-lg-12 + = form.input :email, :as => :email, placeholder: 'user@domain.com' + + = form.button :submit, :value => t("devise.passwords.new.commit"), class: 'btn btn-default formSubmitr', form: 'password_form' diff --git a/app/views/devise/sessions/new.html.slim b/app/views/devise/sessions/new.html.slim index 0ed17e24a..a812726e5 100644 --- a/app/views/devise/sessions/new.html.slim +++ b/app/views/devise/sessions/new.html.slim @@ -1,3 +1,5 @@ +- content_for :page_header_title, t('.title') + .page_content#devise .container-fluid #sessions_new.row diff --git a/app/views/errors/forbidden.html.slim b/app/views/errors/forbidden.html.slim index 23ea67eff..8c35b46a8 100644 --- a/app/views/errors/forbidden.html.slim +++ b/app/views/errors/forbidden.html.slim @@ -9,7 +9,7 @@ p strong = "Désolé, la page demandée la page n'est pas accessible avec votre profil utilisateur." - p = "Vous pouvez néanmoins continuer à utiliser l'application IBOO." + p = "Vous pouvez néanmoins continuer à utiliser l'application." - else p diff --git a/app/views/errors/server_error.html.slim b/app/views/errors/server_error.html.slim index 189a48760..529ad73e8 100644 --- a/app/views/errors/server_error.html.slim +++ b/app/views/errors/server_error.html.slim @@ -9,7 +9,7 @@ p strong = "Désolé, une erreur est survenue." - p = "Vous pouvez néanmoins continuer à utiliser l'application IBOO." + p = "Vous pouvez néanmoins continuer à utiliser l'application." - else p diff --git a/app/views/import_resources/index.html.slim b/app/views/import_resources/index.html.slim index 728d9f4a8..6b4e60026 100644 --- a/app/views/import_resources/index.html.slim +++ b/app/views/import_resources/index.html.slim @@ -43,9 +43,8 @@ end \ ), \ ], - links: [], cls: 'table has-search' - else .col-lg-12 - @import.messages.each do |message| - = I18n.t("import_messages.compliance_check_messages.#{message.message_key}") + = I18n.t("import_messages.#{message.message_key}") diff --git a/app/views/imports/_filters.html.slim b/app/views/imports/_filters.html.slim index b1f23e2c8..25c0d10d9 100644 --- a/app/views/imports/_filters.html.slim +++ b/app/views/imports/_filters.html.slim @@ -1,17 +1,17 @@ = search_form_for @q, url: workbench_imports_path(@workbench), html: { method: :get, class: 'form form-filter' } do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_creator_cont) = f.search_field :name_or_creator_cont, class: 'form-control', placeholder: t('imports.filters.name_or_creator_cont') span.input-group-btn button.btn.btn-default#search_btn type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :status_eq_any) = f.label Import.human_attribute_name(:status), required: false, class: 'control-label' = f.input :status_eq_any, collection: %w(pending successful warning failed), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + import_status(l) + "</span>").html_safe}, required: false, wrapper_html: { class: "checkbox_list"} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :started_at) = f.label Import.human_attribute_name(:started_at), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :started_at do |p| diff --git a/app/views/imports/_import_messages.html.slim b/app/views/imports/_import_messages.html.slim new file mode 100644 index 000000000..af10b23e5 --- /dev/null +++ b/app/views/imports/_import_messages.html.slim @@ -0,0 +1,8 @@ +- if import_messages.any? + ul.list-unstyled.import_message-list + - import_messages.each do | import_message | + li + span(class="#{bootstrap_class_for_message_criticity import_message.criticity}") + = t( ['import_messages', + 'compliance_check_messages', + import_message.message_key].join('.'), import_message.message_attributes.symbolize_keys) diff --git a/app/views/imports/index.html.slim b/app/views/imports/index.html.slim index 79452bbfc..4fc077bd6 100644 --- a/app/views/imports/index.html.slim +++ b/app/views/imports/index.html.slim @@ -1,6 +1,4 @@ - breadcrumb :imports, @workbench -- content_for :page_header_actions do - = link_to(t('imports.actions.new'), new_workbench_import_path(workbench_id: @workbench), class: 'btn btn-primary') .page_content .container-fluid @@ -34,7 +32,6 @@ attribute: 'creator' \ ) \ ], - links: [], cls: 'table has-search' = new_pagination @imports, 'pull-right' diff --git a/app/views/imports/show.html.slim b/app/views/imports/show.html.slim index 5e22e03e0..7a9d02077 100644 --- a/app/views/imports/show.html.slim +++ b/app/views/imports/show.html.slim @@ -1,13 +1,4 @@ - breadcrumb :import, @workbench, @import -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @import.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content - page_header_content_for @import @@ -19,46 +10,51 @@ .row .col-lg-12 - = table_builder_2 @import.children, - [ \ - TableBuilderHelper::Column.new( \ - name: 'Nom du jeu de données', \ - attribute: 'name', \ - sortable: false, \ - link_to: lambda do |import| \ - referential_path(import.referential) if import.referential.present? \ - end \ - ), \ - TableBuilderHelper::Column.new( \ - key: :status, \ - attribute: Proc.new { |n| import_status(n.status) }, \ - sortable: false, \ - link_to: lambda do |import| \ - workbench_import_import_resources_path(import.workbench_id, import) \ - end \ - ), \ - TableBuilderHelper::Column.new( \ - name: 'Contrôle STIF', \ - attribute: '', \ - sortable: false, \ - ), \ - TableBuilderHelper::Column.new( \ - name: 'Contrôle organisation', \ - attribute: '', \ - sortable: false, \ - ) \ - ], - links: [], - cls: 'table', - overhead: [ \ - {}, \ - { \ - title: "#{@import.children_succeedeed} jeu de données validé sur #{@import.children.count} présent(s) dans l'archive", \ - width: 1, \ - cls: "#{@import.import_status_css_class} full-border" \ - }, { \ - title: 'Bilan des jeux de contrôles d\'import <span title="Lorem ipsum..." class="fa fa-lg fa-info-circle text-info"></span>', \ - width: 2, \ - cls: 'overheaded-default colspan="2"' \ - } \ - ] + .error_messages + = render 'import_messages', import_messages: @import.messages + + - if @import.children.any? + .row + .col-lg-12 + = table_builder_2 @import.children, + [ \ + TableBuilderHelper::Column.new( \ + name: 'Nom du jeu de données', \ + attribute: 'name', \ + sortable: false, \ + link_to: lambda do |import| \ + referential_path(import.referential) if import.referential.present? \ + end \ + ), \ + TableBuilderHelper::Column.new( \ + key: :status, \ + attribute: Proc.new { |n| import_status(n.status) }, \ + sortable: false, \ + link_to: lambda do |import| \ + workbench_import_import_resources_path(import.workbench_id, import) \ + end \ + ), \ + TableBuilderHelper::Column.new( \ + name: 'Contrôle STIF', \ + attribute: '', \ + sortable: false, \ + ), \ + TableBuilderHelper::Column.new( \ + name: 'Contrôle organisation', \ + attribute: '', \ + sortable: false, \ + ) \ + ], + cls: 'table', + overhead: [ \ + {}, \ + { \ + title: "#{@import.children_succeedeed} jeu de données validé sur #{@import.children.count} présent(s) dans l'archive", \ + width: 1, \ + cls: "#{@import.import_status_css_class} full-border" \ + }, { \ + title: 'Bilan des jeux de contrôles d\'import <span title="Lorem ipsum..." class="fa fa-lg fa-info-circle text-info"></span>', \ + width: 2, \ + cls: 'overheaded-default colspan="2"' \ + } \ + ] diff --git a/app/views/journey_patterns_collections/show.html.slim b/app/views/journey_patterns_collections/show.html.slim index 834501da3..8a7b0c47c 100644 --- a/app/views/journey_patterns_collections/show.html.slim +++ b/app/views/journey_patterns_collections/show.html.slim @@ -17,6 +17,7 @@ | window.stopPoints = #{(@stop_points_list.to_json).html_safe}; | window.journeyPatternLength = #{@journey_patterns.total_entries()}; | window.journeyPatternsPerPage = #{@ppage}; - | window.perms = #{raw @perms} + | window.perms = #{raw @perms}; + | window.features = #{raw @features}; = javascript_pack_tag 'journey_patterns/index.js' diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 567e14ef0..3921c8701 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -13,6 +13,8 @@ html lang=I18n.locale = javascript_pack_tag 'application' = javascript_include_tag 'application' + = javascript_tag do + | I18n.locale = '#{I18n.locale}' body = render 'layouts/navigation/main_nav' @@ -21,3 +23,6 @@ html lang=I18n.locale = yield #sidebar = yield :sidebar + + = render 'shared/development_toolbar' + = yield :javascript diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 000000000..977cbfe5b --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + body { + font-family: "Open Sans", sans-serif; + font-size: 14px; + line-height: 1.4; + color: #4b4b4b; + width: 800px; + } + </style> + </head> + + <body> + <%= image_tag 'mail-header.png' %> + + <%= yield %> + </body> +</html> diff --git a/app/views/layouts/mailer.html.slim b/app/views/layouts/mailer.html.slim deleted file mode 100644 index 5db6917b3..000000000 --- a/app/views/layouts/mailer.html.slim +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - meta charset="utf-8" - /!* Email styles need to be inline */ - body - = yield diff --git a/app/views/layouts/navigation/_main_nav_left_content_stif.html.slim b/app/views/layouts/navigation/_main_nav_left_content_stif.html.slim index 3963d4cd4..1b7293d21 100644 --- a/app/views/layouts/navigation/_main_nav_left_content_stif.html.slim +++ b/app/views/layouts/navigation/_main_nav_left_content_stif.html.slim @@ -29,7 +29,7 @@ span Jeux de données = link_to workbench_imports_path(current_offer_workbench), class: "list-group-item #{(params[:controller] == 'imports') ? 'active' : ''}" do span Import - = link_to calendars_path, class: 'list-group-item' do + = link_to workgroup_calendars_path(current_workgroup), class: 'list-group-item' do span Modèles de calendrier = link_to workbench_compliance_check_sets_path(current_offer_workbench), class: 'list-group-item' do span Rapport de contrôle diff --git a/app/views/layouts/navigation/_main_nav_top.html.slim b/app/views/layouts/navigation/_main_nav_top.html.slim index d6c849d3f..f664d5416 100644 --- a/app/views/layouts/navigation/_main_nav_top.html.slim +++ b/app/views/layouts/navigation/_main_nav_top.html.slim @@ -1,5 +1,5 @@ .nav-menu#menu_top - .brandname = t('brandname') + .brandname = link_to t('brandname'), root_path .menu-content .menu-item @@ -11,12 +11,16 @@ span.fa.fa-lg.fa-tasks = link_to '#', class: 'menu-item', data: { panel: 'toggle', target: '#profile_panel' }, title: 'Profil' do - span = current_user.username + span = current_user.name span.fa.fa-lg.fa-user + - if Rails.application.config.development_toolbar + = link_to '#', data: { toggle: 'modal', target: '#development-toolbar' }, class: "toolbar-button menu-item" do + .fa.fa-cog = link_to destroy_user_session_path, method: :delete, class: 'menu-item', title: 'Se déconnecter' do span.fa.fa-lg.fa-sign-out + = render 'layouts/navigation/nav_panel_operations' = render 'layouts/navigation/nav_panel_profile' if user_signed_in? diff --git a/app/views/layouts/navigation/_page_header.html.slim b/app/views/layouts/navigation/_page_header.html.slim index 90fd7d855..e407e53da 100644 --- a/app/views/layouts/navigation/_page_header.html.slim +++ b/app/views/layouts/navigation/_page_header.html.slim @@ -1,23 +1,36 @@ -div.page_header - div.container-fluid - div.row - div.col-lg-9.col-md-8.col-sm-7.col-xs-7 +- action_links = resource.action_links(params[:action]) rescue nil +- action_links ||= decorated_collection.action_links(params[:action]) rescue nil + +.page_header + .container-fluid + .row + .col-lg-9.col-md-8.col-sm-7.col-xs-7 - if defined?(resource_class) - div.page-icon + .page-icon span.sb class="sb-#{resource_class.model_name.name.underscore}" - div.page-title + .page-title - if content_for? :page_header_title h1 = yield :page_header_title - else - if defined?(resource_class) h1 = t("#{resource_class.model_name.name.underscore.pluralize}.#{params[:action]}.title") - div.col-lg-3.col-md-4.col-sm-5.col-xs-5.text-right - div.page-action + .col-lg-3.col-md-4.col-sm-5.col-xs-5.text-right + .page-action - if content_for? :page_header_meta = yield :page_header_meta - if content_for? :page_header_actions = yield :page_header_actions + - if action_links&.primary&.any? + - action_links.primary.each do |link| + = link.to_html do |l| + - l.class "btn btn-default #{l.disabled ? "disabled" : ""}" + - if action_links&.secondary&.any? + .row.mb-sm + .col-lg-12.text-right + - action_links.secondary.each do |link| + = link.to_html do |l| + - l.class "btn btn-primary #{l.disabled ? "disabled" : ""}" - if content_for? :page_header_content = yield :page_header_content diff --git a/app/views/layouts/snapshots/actions_links.html.slim b/app/views/layouts/snapshots/actions_links.html.slim new file mode 100644 index 000000000..f1fa55e87 --- /dev/null +++ b/app/views/layouts/snapshots/actions_links.html.slim @@ -0,0 +1,21 @@ +doctype html +html lang=I18n.locale + head + meta charset="utf-8" + meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" + + = csrf_meta_tag + + title = t('brandname') + + = stylesheet_link_tag 'base' + = stylesheet_link_tag 'application' + + = javascript_pack_tag 'application' + = javascript_include_tag 'application' + + body + = render 'layouts/navigation/main_nav' + = render 'layouts/flash_messages', flash: flash + div.page_header + = yield diff --git a/app/views/layouts/snapshots/default.html.slim b/app/views/layouts/snapshots/default.html.slim new file mode 100644 index 000000000..9e4565dcb --- /dev/null +++ b/app/views/layouts/snapshots/default.html.slim @@ -0,0 +1,19 @@ +doctype html +html lang=I18n.locale + head + meta charset="utf-8" + meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" + + = csrf_meta_tag + + title = t('brandname') + + = stylesheet_link_tag 'base' + = stylesheet_link_tag 'application' + + = javascript_pack_tag 'application' + = javascript_include_tag 'application' + + body + = yield + diff --git a/app/views/line_footnotes/show.html.slim b/app/views/line_footnotes/show.html.slim index e4f2a1d42..29e1f5708 100644 --- a/app/views/line_footnotes/show.html.slim +++ b/app/views/line_footnotes/show.html.slim @@ -16,7 +16,11 @@ .panel-heading = footnote.code .panel-body p = footnote.label + .panel-footer.text-right + div + p.text-muted.small = Chouette::Footnote.human_attribute_name(:checksum) + p.text-muted.small = footnote.checksum p.text-muted em.small = "Dernière mise à jour le #{l(footnote.updated_at, format: :short)}" diff --git a/app/views/line_referentials/show.html.slim b/app/views/line_referentials/show.html.slim index b4b32bc52..763eb076e 100644 --- a/app/views/line_referentials/show.html.slim +++ b/app/views/line_referentials/show.html.slim @@ -1,7 +1,8 @@ - breadcrumb :line_referential, @line_referential - page_header_content_for @line_referential -- content_for :page_header_actions do - = link_to(t('actions.sync'), sync_line_referential_path(@line_referential), method: :post, class: 'btn btn-default') +- if policy(@line_referential).synchronize? + - content_for :page_header_actions do + = link_to(t('actions.sync'), sync_line_referential_path(@line_referential), method: :post, class: 'btn btn-default') - content_for :page_header_content do .row.mb-md diff --git a/app/views/lines/_filters.html.slim b/app/views/lines/_filters.html.slim index e3674656a..67ba297cf 100644 --- a/app/views/lines/_filters.html.slim +++ b/app/views/lines/_filters.html.slim @@ -1,25 +1,25 @@ = search_form_for @q, url: line_referential_lines_path(@line_referential), html: {method: :get}, class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_number_or_objectid_cont) = f.search_field :name_or_number_or_objectid_cont, placeholder: t('lines.index.name_or_number_or_objectid'), class: 'form-control' span.input-group-btn button.btn.btn-default#search-btn type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :network_id_eq_any) = f.label Chouette::Line.human_attribute_name(:network_id), required: false, class: 'control-label' = f.input :network_id_eq_any, collection: @line_referential.networks.order(name: :asc), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :company_id_eq_any) = f.label Chouette::Line.human_attribute_name(:company_id), required: false, class: 'control-label' = f.input :company_id_eq_any, collection: @line_referential.companies.order(name: :asc), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :transport_mode_eq_any) = f.label Chouette::Line.human_attribute_name(:transport_mode), required: false, class: 'control-label' = f.input :transport_mode_eq_any, collection: StifTransportModeEnumerations.sorted_transport_modes, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_mode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :transport_submode_eq_any) = f.label Chouette::Line.human_attribute_name(:transport_submode), required: false, class: 'control-label' = f.input :transport_submode_eq_any, collection: StifTransportSubmodeEnumerations.sorted_transport_submodes, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_submode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} diff --git a/app/views/lines/_form.html.slim b/app/views/lines/_form.html.slim index de0308289..909d6512e 100644 --- a/app/views/lines/_form.html.slim +++ b/app/views/lines/_form.html.slim @@ -3,7 +3,7 @@ .col-lg-12 = f.input :name = f.input :network_id, as: :select, :collection => @line_referential.networks, include_blank: false - = f.input :company_id, as: :select, :collection => @line_referential.companies, include_blank: false + = f.input :company_id, as: :select, :collection => @line_referential.companies, include_blank: true = f.input :secondary_company_ids, :collection => @line_referential.companies, include_blank: false, input_html: { multiple: true, 'data-select2ed': true }, label: t('activerecord.attributes.line.secondary_company') = f.input :published_name = f.input :registration_number @@ -20,4 +20,3 @@ .separator = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'lines_form' - diff --git a/app/views/lines/index.html.slim b/app/views/lines/index.html.slim index 8b035b477..2d64e5f73 100644 --- a/app/views/lines/index.html.slim +++ b/app/views/lines/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :lines, @line_referential -- content_for :page_header_actions do - - if (policy(Chouette::Line).create? && @line_referential.organisations.include?(current_organisation)) - = link_to(t('lines.actions.new'), new_line_referential_line_path(@line_referential), class: 'btn btn-primary') .page_content .container-fluid @@ -41,7 +38,7 @@ ), \ TableBuilderHelper::Column.new( \ key: 'companies.name', \ - attribute: Proc.new { |n| n.try(:company).try(:name) } \ + attribute: Proc.new { |n| n&.company&.name || "-" } \ ), \ TableBuilderHelper::Column.new( \ key: :transport_mode, \ @@ -52,7 +49,6 @@ attribute: Proc.new { |n| n.transport_submode.present? ? t("enumerize.transport_submode.#{n.try(:transport_submode)}") : "-" } \ ) \ ], - links: [:show], cls: 'table has-filter has-search' = new_pagination @lines, 'pull-right' diff --git a/app/views/lines/show.html.slim b/app/views/lines/show.html.slim index d62fe30d6..34b907bdd 100644 --- a/app/views/lines/show.html.slim +++ b/app/views/lines/show.html.slim @@ -1,14 +1,4 @@ - breadcrumb :line, @line -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @line.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content - - page_header_content_for @line .page_content diff --git a/app/views/merges/_form.html.slim b/app/views/merges/_form.html.slim new file mode 100644 index 000000000..b97a4b03e --- /dev/null +++ b/app/views/merges/_form.html.slim @@ -0,0 +1,12 @@ += simple_form_for merge, as: :merge, url: workbench_merges_path(workbench), html: {class: 'form-horizontal', id: 'wb_merge_form'}, wrapper: :horizontal_form do |form| + + .row + .col-lg-12 + = form.input :referentials, :collection => @mergeable_referentials, include_blank: false, input_html: { multiple: true, 'data-select2ed': true } + + + .row + .col-lg-12 + = form.input :creator, input_html: { disabled: true } + + = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'wb_merge_form' diff --git a/app/views/merges/new.html.slim b/app/views/merges/new.html.slim new file mode 100644 index 000000000..dab4bdf4e --- /dev/null +++ b/app/views/merges/new.html.slim @@ -0,0 +1,7 @@ +- breadcrumb :merges, @workbench + +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = render 'form', merge: @merge, workbench: @workbench diff --git a/app/views/merges/show.html.slim b/app/views/merges/show.html.slim new file mode 100644 index 000000000..a94552c0d --- /dev/null +++ b/app/views/merges/show.html.slim @@ -0,0 +1,17 @@ +- breadcrumb :merge, @merge +- page_header_content_for @merge + +.page_content + .container-fluid + .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list t('metadatas'), + { @merge.class.human_attribute_name(:referentials) => @merge.referentials.map(&:name).join(', '), + @merge.class.human_attribute_name(:operator) => @merge.creator, + @merge.class.human_attribute_name(:status) => @merge.status, + @merge.class.human_attribute_name(:created_at) => @merge.created_at ? l(@merge.created_at) : '-', + @merge.class.human_attribute_name(:started_at) => @merge.started_at ? l(@merge.started_at) : '-', + @merge.class.human_attribute_name(:ended_at) => @merge.ended_at ? l(@merge.ended_at) : '-' } + + - if resource.output.current + = referential_overview resource.output.current diff --git a/app/views/networks/_form.html.slim b/app/views/networks/_form.html.slim index 362584f97..f91a112e8 100644 --- a/app/views/networks/_form.html.slim +++ b/app/views/networks/_form.html.slim @@ -4,11 +4,11 @@ = f.input :name, :input_html => { :title => t("formtastic.titles#{format_restriction_for_locales(@line_referential)}.network.name")} = f.input :registration_number, :input_html => { :title => t("formtastic.titles#{format_restriction_for_locales(@line_referential)}.network.registration_number")} = f.input :comment - = f.input :version_date, :label_html => { :class => 'string optional col-sm-4 col-xs-5 control-label' }, :wrapper => :multi_select_inline - = f.input :description - = f.input :source_name - = f.input :source_type_name, as: :select, :collection => Chouette::Network.source_type_name.options, :include_blank => true - = f.input :source_identifier + / = f.input :version_date, :label_html => { :class => 'string optional col-sm-4 col-xs-5 control-label' }, :wrapper => :multi_select_inline + / = f.input :description + / = f.input :source_name + / = f.input :source_type_name, as: :select, :collection => Chouette::Network.source_type_name.options, :include_blank => true + / = f.input :source_identifier .separator = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'network_form' diff --git a/app/views/networks/index.html.slim b/app/views/networks/index.html.slim index b13c73e9e..6aeb140cc 100644 --- a/app/views/networks/index.html.slim +++ b/app/views/networks/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :networks, @line_referential -- content_for :page_header_actions do - - if policy(Chouette::Network).create? - = link_to(t('networks.actions.new'), new_line_referential_network_path(@line_referential), class: 'btn btn-primary') .page_content .container-fluid diff --git a/app/views/networks/show.html.slim b/app/views/networks/show.html.slim index f7d40a049..527fb8d5b 100644 --- a/app/views/networks/show.html.slim +++ b/app/views/networks/show.html.slim @@ -1,17 +1,13 @@ - breadcrumb :network, @network - page_header_content_for @network -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @network.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content + .page_content .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 = definition_list t('metadatas'), - { 'ID Codif' => @network.try(:get_objectid).try(:short_id) } + { @network.human_attribute_name(:id) => @network.get_objectid.try(:short_id), \ + @network.human_attribute_name(:name) => @network.name, \ + @network.human_attribute_name(:registration_number) => @network.registration_number, \ + @network.human_attribute_name(:comment) => (@network.comment.presence || '-'), \ + } diff --git a/app/views/purchase_windows/_date_value_fields.html.slim b/app/views/purchase_windows/_date_value_fields.html.slim new file mode 100644 index 000000000..7bde06a94 --- /dev/null +++ b/app/views/purchase_windows/_date_value_fields.html.slim @@ -0,0 +1,13 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + + .wrapper + div + = f.input :value, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/_filters.html.slim b/app/views/purchase_windows/_filters.html.slim new file mode 100644 index 000000000..eedbf31d5 --- /dev/null +++ b/app/views/purchase_windows/_filters.html.slim @@ -0,0 +1,15 @@ += search_form_for @q, url: referential_purchase_windows_path, builder: SimpleForm::FormBuilder, html: { method: :get, class: 'form form-filter' } do |f| + .ffg-row + .input-group.search_bar class=filter_item_class(params[:q], :name_cont) + = f.search_field :name_cont, class: 'form-control', placeholder: t('purchase_windows.index.filter_placeholder') + span.input-group-btn + button.btn.btn-default#search_btn type='submit' + span.fa.fa-search + + .form-group class=filter_item_class(params[:q], :contains_date) + = f.label Chouette::PurchaseWindow.human_attribute_name(:date), class: 'control-label' + = f.input :contains_date, as: :date, label: false, wrapper_html: { class: 'date smart_date' }, class: 'form-control', default: @date, include_blank: @date ? false : true + + .actions + = link_to t('actions.erase'), referential_purchase_windows_path, class: 'btn btn-link' + = f.submit t('actions.filter'), id: 'purchase_window_filter_btn', class: 'btn btn-default' diff --git a/app/views/purchase_windows/_form.html.slim b/app/views/purchase_windows/_form.html.slim new file mode 100644 index 000000000..2101ae6db --- /dev/null +++ b/app/views/purchase_windows/_form.html.slim @@ -0,0 +1,29 @@ += simple_form_for [@referential, @purchase_window], html: { class: 'form-horizontal', id: 'purchase_window_form' }, wrapper: :horizontal_form do |f| + .row + .col-lg-12 + = f.input :name + = f.input :color, as: :color_select, collection: Chouette::PurchaseWindow.colors_i18n + + .separator + + .row + .col-lg-12 + .subform + .nested-head + .wrapper + div + .form-group + label.control-label + = t('simple_form.labels.purchase_window.ranges.begin') + div + .form-group + label.control-label + = t('simple_form.labels.purchase_window.ranges.end') + div + + = f.simple_fields_for :periods do |period| + = render 'period_fields', f: period + .links.nested-linker + = link_to_add_association t('simple_form.labels.purchase_window.add_a_date_range'), f, :periods, class: 'btn btn-outline-primary' + + = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'purchase_window_form' diff --git a/app/views/purchase_windows/_period_fields.html.slim b/app/views/purchase_windows/_period_fields.html.slim new file mode 100644 index 000000000..95e204554 --- /dev/null +++ b/app/views/purchase_windows/_period_fields.html.slim @@ -0,0 +1,15 @@ +.nested-fields + - if f.object.errors.has_key? :base + .row + .col-lg-12 + .alert.alert-danger + - f.object.errors[:base].each do |message| + p.small = message + + .wrapper + div + = f.input :begin, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = f.input :end, as: :date, label: false, wrapper_html: { class: 'date smart_date' } + div + = link_to_remove_association '', f, class: 'fa fa-trash', data: { confirm: 'Etes-vous sûr(e) ?' }, title: t('actions.delete') diff --git a/app/views/purchase_windows/edit.html.slim b/app/views/purchase_windows/edit.html.slim new file mode 100644 index 000000000..6354db853 --- /dev/null +++ b/app/views/purchase_windows/edit.html.slim @@ -0,0 +1,7 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = render 'form' diff --git a/app/views/purchase_windows/index.html.slim b/app/views/purchase_windows/index.html.slim new file mode 100644 index 000000000..0dba86935 --- /dev/null +++ b/app/views/purchase_windows/index.html.slim @@ -0,0 +1,41 @@ +- breadcrumb :purchase_windows, @referential + +.page_content + .container-fluid + - if params[:q].present? or @purchase_windows.any? + .row + .col-lg-12 + = render 'filters' + + - if @purchase_windows.any? + .row + .col-lg-12 + = table_builder_2 @purchase_windows, + [ \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'name', \ + link_to: lambda do |purchase_window| \ + referential_purchase_window_path(purchase_window.referential, purchase_window) \ + end \ + ), \ + TableBuilderHelper::Column.new( \ + key: :color, \ + attribute: Proc.new { |tt| tt.color ? content_tag(:span, '', class: 'fa fa-circle', style: "color:#{tt.color}") : '-' }\ + ), \ + TableBuilderHelper::Column.new( \ + key: :bounding_dates, \ + attribute: Proc.new {|w| w.bounding_dates.nil? ? '-' : t('validity_range', debut: l(w.bounding_dates.begin, format: :short), end: l(w.bounding_dates.end, format: :short))}, \ + sortable: false \ + ) \ + ], + cls: 'table has-filter' + + = new_pagination @purchase_windows, 'pull-right' + + - unless @purchase_windows.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('purchase_windows.search_no_results') + += javascript_pack_tag 'date_filters' diff --git a/app/views/purchase_windows/new.html.slim b/app/views/purchase_windows/new.html.slim new file mode 100644 index 000000000..402084167 --- /dev/null +++ b/app/views/purchase_windows/new.html.slim @@ -0,0 +1,6 @@ +- breadcrumb :purchase_windows, @referential +.page_content + .container-fluid + .row + .col-lg-8.col-lg-offset-2.col-md-8.col-md-offset-2.col-sm-10.col-sm-offset-1 + = render 'form' diff --git a/app/views/purchase_windows/show.html.slim b/app/views/purchase_windows/show.html.slim new file mode 100644 index 000000000..4e836f424 --- /dev/null +++ b/app/views/purchase_windows/show.html.slim @@ -0,0 +1,12 @@ +- breadcrumb :purchase_window, @referential, @purchase_window +- page_header_content_for @purchase_window + +.page_content + .container-fluid + .row + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + = definition_list t('metadatas'), + { Chouette::PurchaseWindow.human_attribute_name(:name) => @purchase_window.try(:name), + 'Organisation' => @purchase_window.referential.organisation.name, + Chouette::PurchaseWindow.human_attribute_name(:date_ranges) => @purchase_window.periods.map{|d| t('validity_range', debut: l(d.begin, format: :short), end: l(d.end, format: :short))}.join('<br>').html_safe, + Chouette::PurchaseWindow.human_attribute_name(:checksum) => @purchase_window.checksum } diff --git a/app/views/referential_companies/index.html.slim b/app/views/referential_companies/index.html.slim index de0f7de69..3bff448c7 100644 --- a/app/views/referential_companies/index.html.slim +++ b/app/views/referential_companies/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :referential_companies, @referential -- content_for :page_header_actions do - - if policy(Chouette::Company).create? - = link_to(t('companies.actions.new'), new_referential_company_path(@referential), class: 'btn btn-default') .page_content .container-fluid @@ -46,7 +43,6 @@ attribute: 'url' \ ) \ ], - links: [:show, :edit], cls: 'table has-search' = new_pagination @companies, 'pull-right' diff --git a/app/views/referential_lines/_filters.html.slim b/app/views/referential_lines/_filters.html.slim index 379d6234f..501f61c16 100644 --- a/app/views/referential_lines/_filters.html.slim +++ b/app/views/referential_lines/_filters.html.slim @@ -1,13 +1,13 @@ = search_form_for @q, url: referential_line_path(@referential, @line), class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_objectid_cont) = f.search_field :name_or_objectid_cont, class: 'form-control', placeholder: "Indiquez un nom d'itinéraire ou un ID..." span.input-group-btn button.btn.btn-default#search-btn type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :wayback_eq_any) = f.label Chouette::Route.human_attribute_name(:wayback), required: false, class: 'control-label' = f.input :wayback_eq_any, class: 'form-control', collection: Chouette::Route.wayback.values, as: :check_boxes, label: false, required: false, wrapper_html: { class: 'checkbox_list'}, label_method: lambda{|l| ("<span>" + t("enumerize.route.wayback.#{l}") + "</span>").html_safe} diff --git a/app/views/referential_lines/show.html.slim b/app/views/referential_lines/show.html.slim index cfba8cab3..5ea0e31bb 100644 --- a/app/views/referential_lines/show.html.slim +++ b/app/views/referential_lines/show.html.slim @@ -1,13 +1,4 @@ - breadcrumb :referential_line, @referential, @line -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @line.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content - page_header_content_for @line .page_content @@ -26,7 +17,8 @@ @line.human_attribute_name(:transport_submode) => (@line.transport_submode.present? ? t("enumerize.transport_submode.#{@line.transport_submode}") : '-'), @line.human_attribute_name(:url) => (@line.url ? @line.url : '-'), @line.human_attribute_name(:seasonal) => (@line.seasonal? ? t('true') : t('false')),} - + .col-lg-6.col-md-6.col-sm-12.col-xs-12 + #routes_map.map.mb-lg .row .col-lg-12 .h3 = t('lines.show.routes.title') @@ -79,8 +71,8 @@ attribute: Proc.new{ |r| r.try(:journey_patterns).count } \ ) \ ], - links: [:show, :edit], - cls: 'table has-search' + cls: 'table has-search', + action: :index = new_pagination @routes, 'pull-right' @@ -88,3 +80,8 @@ .row.mt-xs .col-lg-12 = replacement_msg t('routes.search_no_results') + += javascript_tag do + | window.routes = "#{URI.escape(@routes.map{|r| {name: r.name, id: r.id, stops: route_json_for_edit(r, serialize: false)}}.to_json)}" + += javascript_pack_tag 'referential_lines/show.js' diff --git a/app/views/referential_networks/index.html.slim b/app/views/referential_networks/index.html.slim index d556e7e5e..efa28dc05 100644 --- a/app/views/referential_networks/index.html.slim +++ b/app/views/referential_networks/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :referential_networks, @referential -- if policy(Chouette::Network).create? - - content_for :page_header_actions do - = link_to(t('networks.actions.new'), new_referential_network_path(@referential), class: 'btn btn-default') .page_content .container-fluid diff --git a/app/views/referential_networks/show.html.slim b/app/views/referential_networks/show.html.slim index 7de304671..3d13d9211 100644 --- a/app/views/referential_networks/show.html.slim +++ b/app/views/referential_networks/show.html.slim @@ -1,14 +1,5 @@ - breadcrumb :referential_network, @referential, @network - page_header_content_for @network -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @network.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content / PageContent .page_content diff --git a/app/views/referential_stop_areas/_form.html.slim b/app/views/referential_stop_areas/_form.html.slim index 50f5d4aaf..8181ec3f3 100644 --- a/app/views/referential_stop_areas/_form.html.slim +++ b/app/views/referential_stop_areas/_form.html.slim @@ -7,7 +7,7 @@ = form.inputs do = form.input :id, as: :hidden = form.input :name, :input_html => { :title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.name")} - = form.input :stop_area_type, as: :select, :input_html => { :disabled => !@stop_area.new_record? }, :collection => Chouette::StopArea.area_type.options, :include_blank => false } + = form.input :stop_area_type, as: :select, :input_html => { :disabled => !@stop_area.new_record? }, :collection => Chouette::StopArea.area_type.options, :include_blank => false .location_info h3 = t("stop_areas.stop_area.localisation") diff --git a/app/views/referential_stop_areas/show.html.slim b/app/views/referential_stop_areas/show.html.slim index 0470b4654..cb04ab7a6 100644 --- a/app/views/referential_stop_areas/show.html.slim +++ b/app/views/referential_stop_areas/show.html.slim @@ -1,14 +1,5 @@ - breadcrumb :referential_stop_area, @referential, @stop_area - page_header_content_for @stop_area -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @stop_area.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content .page_content .container-fluid diff --git a/app/views/referential_vehicle_journeys/_filters.html.slim b/app/views/referential_vehicle_journeys/_filters.html.slim new file mode 100644 index 000000000..3104e3a71 --- /dev/null +++ b/app/views/referential_vehicle_journeys/_filters.html.slim @@ -0,0 +1,62 @@ += search_form_for @q, url: referential_vehicle_journeys_path(@referential), html: {method: :get}, class: 'form form-filter' do |f| + .ffg-row + .input-group.search_bar + = f.search_field :published_journey_name_or_objectid_cont, placeholder: t('.published_journey_name_or_objectid'), class: 'form-control' + span.input-group-btn + button.btn.btn-default#search-btn type='submit' + span.fa.fa-search + .ffg-row + .form-group.per-page-select + = I18n.t("simple_form.per_page") + = %w(10 50 100).each_with_index.map{ |v, i| (params[:per_page] == v || params[:per_page].nil? && i == 0) ? "<span class='selected'>#{v}</span>" : link_to(v, referential_vehicle_journeys_path(@referential, q: params[:q], per_page: v)) }.join(' - ').html_safe + .form-group.togglable class=filter_item_class(params[:q], :company_id_eq_any) + = f.label Chouette::VehicleJourney.human_attribute_name(:company), required: false, class: 'control-label' + - if @all_companies.present? + = f.input :company_id_eq_any, collection: @all_companies.select(:id, :name).order(name: :asc), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} + - else + = f.input :company_id_eq_any, collection: [[I18n.t('companies.search_no_results_for_filter'), nil]], as: :check_boxes, label: false, disabled: true, required: false, wrapper_html: { class: 'checkbox_list disabled'} + + .form-group.togglable class=filter_item_class(params[:q], :route_line_id_eq) + = f.label Chouette::Line.model_name.human, + required: false, + class: 'control-label' + .form-inline.filter_menu + = f.input :route_line_id_eq, + as: :select, + include_blank: t(".all"), + collection: @vehicle_journeys.lines, + selected: params[:q] && params[:q][:route_line_id_eq], + input_html: { \ + 'data-select2ed': 'true', + 'data-select2ed-placeholder': t('referentials.filters.line') \ + }, + label: false, + label_method: :display_name, + wrapper_html: { class: 'filter_menu-item select2ed' } + + .form-group.togglable.name-filter class=filter_item_class(params[:q], :published_journey_name_gteq) + = f.label Chouette::VehicleJourney.human_attribute_name(:published_journey_name), required: false, class: 'control-label' + .inputs.form-inline.checkbox_list + = f.input :published_journey_name_gteq, label: false, wrapper_html: { class: 'w45'} + .form-group.w10.to= I18n.t('vehicle_journeys.form.to') + = f.input :published_journey_name_lteq, label: false, wrapper_html: { class: 'w45'} + .form-group.togglable class=filter_item_class(params[:q], :stop_area_ids) + = f.label Chouette::StopArea.model_name.human.pluralize, required: false, class: 'control-label' + = f.input :stop_area_ids, collection: @all_stop_areas.select(:id, :name).order(name: :asc), checked: params[:q] && params[:q][:stop_area_ids], as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'}, multiple: true + .form-group.togglable class=filter_item_class(params[:q], :purchase_window) + = f.label Chouette::VehicleJourney.human_attribute_name(:purchase_window), class: 'control-label' + .filter_menu + = f.simple_fields_for :purchase_window do |p| + = p.input :start_date, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @purchase_window_begin_range, include_blank: @purchase_window_begin_range ? false : true + = p.input :end_date, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @purchase_window_end_range, include_blank: @purchase_window_end_range ? false : true + .form-group.togglable class=filter_item_class(params[:q], :time_table) + = f.label Chouette::TimeTable.model_name.human, class: 'control-label' + .filter_menu + = f.simple_fields_for :time_table do |p| + = p.input :start_date, as: :date, label: t('simple_form.from'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @time_table_begin_range, include_blank: @time_table_begin_range ? false : true + = p.input :end_date, as: :date, label: t('simple_form.to'), wrapper_html: { class: 'date smart_date filter_menu-item' }, default: @time_table_end_range, include_blank: @time_table_end_range ? false : true + + + .actions + = link_to 'Effacer', referential_vehicle_journeys_path(@referential), class: 'btn btn-link' + = f.submit 'Filtrer', class: 'btn btn-default' diff --git a/app/views/referential_vehicle_journeys/index.html.slim b/app/views/referential_vehicle_journeys/index.html.slim new file mode 100644 index 000000000..69e29597c --- /dev/null +++ b/app/views/referential_vehicle_journeys/index.html.slim @@ -0,0 +1,59 @@ +- breadcrumb :referential_vehicle_journeys, @referential +- content_for :page_header_title, t('.title') + +.page_content + .container-fluid + - if params[:q].present? or @vehicle_journeys.present? + .row + .col-lg-12 + = render 'filters' + + - if @vehicle_journeys.present? + .row + .col-lg-12 + .select_table + = table_builder_2 @vehicle_journeys, + [ \ + TableBuilderHelper::Column.new( \ + name: t('objectid'), \ + attribute: Proc.new { |n| n.get_objectid.short_id }, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :published_journey_name, \ + attribute: 'published_journey_name', \ + link_to: lambda do |vehicle_journey| \ + vehicle_journey.published_journey_name ? referential_line_route_vehicle_journeys_path(@referential, vehicle_journey.route.line, vehicle_journey.route) : '' \ + end, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :line, \ + attribute: Proc.new {|v| v.route.line.name}, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :route, \ + attribute: Proc.new {|v| v.route.name}, \ + sortable: false \ + ), \ + TableBuilderHelper::Column.new( \ + key: :departure_time, \ + attribute: Proc.new {|v| v.vehicle_journey_at_stops.first&.departure }, \ + sortable: false \ + ), \ + @filters_stop_areas&.map{|s| table_builder_column_for_stop_area(s)}, + TableBuilderHelper::Column.new( \ + key: :arrival_time, \ + attribute: Proc.new {|v| v.vehicle_journey_at_stops.last&.arrival }, \ + sortable: false \ + ), \ + ].flatten.compact, + cls: 'table has-filter has-search' + + = new_pagination @vehicle_journeys, 'pull-right' + + - unless @vehicle_journeys.any? + .row.mt-xs + .col-lg-12 + = replacement_msg t('.search_no_results') diff --git a/app/views/referentials/_filters.html.slim b/app/views/referentials/_filters.html.slim index c5b6042f0..190e70ebe 100644 --- a/app/views/referentials/_filters.html.slim +++ b/app/views/referentials/_filters.html.slim @@ -1,22 +1,22 @@ = search_form_for @q, url: referential_path(@referential.id), class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_number_or_objectid_cont) = f.search_field :name_or_number_or_objectid_cont, class: 'form-control', placeholder: t('.name_or_number_or_objectid') span.input-group-btn button.btn.btn-default type='submit' span.fa.fa-search .ffg-row - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :transport_mode_eq_any) = f.label Chouette::Line.human_attribute_name(:transport_mode), required: false, class: 'control-label' = f.input :transport_mode_eq_any, collection: @referential.lines.pluck(:transport_mode).uniq.compact, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_mode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } - .form-group.togglable - = f.label Chouette::Line.human_attribute_name(:network), required: false, class: 'control-label' + .form-group.togglable class=filter_item_class(params[:q], :network_id_eq_any) + = f.label t('activerecord.attributes.referential.networks'), required: false, class: 'control-label' = f.input :network_id_eq_any, collection: LineReferential.first.networks.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.networks.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } - .form-group.togglable - = f.label Chouette::Line.human_attribute_name(:company), required: false, class: 'control-label' + .form-group.togglable class=filter_item_class(params[:q], :company_id_eq_any) + = f.label t('activerecord.attributes.referential.companies'), required: false, class: 'control-label' = f.input :company_id_eq_any, collection: LineReferential.first.companies.order('name').pluck(:id), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>#{LineReferential.first.companies.find(l).name}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .actions diff --git a/app/views/referentials/_form.html.slim b/app/views/referentials/_form.html.slim index 6f7da84c7..9927f05bd 100644 --- a/app/views/referentials/_form.html.slim +++ b/app/views/referentials/_form.html.slim @@ -17,7 +17,7 @@ .row .col-lg-12 - if @referential.errors.has_key? :metadatas - .row + .row.metadatas-errors .col-lg-12 .alert.alert-danger - @referential.errors[:metadatas].each do |msg| @@ -51,4 +51,8 @@ .hidden = form.input :workbench_id, as: :hidden - = form.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'referential_form' + = form.button :submit, + t('actions.submit'), + class: 'btn btn-default formSubmitr', + data: { disable_with: t('actions.processing') }, + form: 'referential_form' diff --git a/app/views/referentials/_overview.html.slim b/app/views/referentials/_overview.html.slim new file mode 100644 index 000000000..143784800 --- /dev/null +++ b/app/views/referentials/_overview.html.slim @@ -0,0 +1,75 @@ +.referential-overview id=overview.pagination_param_name + .filters + = search_form_for overview.search, as: overview.search_param_name, url: "##{overview.pagination_param_name}", html: {method: :get}, class: 'form form-filter' do |f| + .ffg-row + .form-group.input-group.search_bar + = f.search_field :name_or_number_or_objectid_cont, placeholder: t('lines.index.name_or_number_or_objectid'), class: 'form-control' + span.input-group-btn + button.btn.btn-default#search-btn type='submit' + span.fa.fa-search + .form-group.togglable + = f.label Chouette::Line.human_attribute_name(:company_id), required: false, class: 'control-label' + = f.input :company_id_eq_any, collection: overview.referential_lines.map(&:company).uniq.sort_by(&:name), as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + l.name + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} + + .form-group.togglable + = f.label Chouette::Line.human_attribute_name(:transport_mode), required: false, class: 'control-label' + = f.input :transport_mode_eq_any, collection: overview.referential_lines.map(&:transport_mode).uniq.sort, as: :check_boxes, label: false, label_method: lambda{|l| ("<span>" + t("enumerize.transport_mode.#{l}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list'} + + .actions + = link_to 'Effacer', url_for() + "##{overview.pagination_param_name}", class: 'btn btn-link' + = f.submit 'Filtrer', class: 'btn btn-default' + + .time-travel + .btn-group + = link_to I18n.t("referentials.overview.head.prev_page"), '#', class: "prev-page btn btn-default disabled" + = link_to I18n.t("referentials.overview.head.today"), '#', class: "today btn btn-default #{overview.includes_today? ? '' : 'disabled'}" + = link_to I18n.t("referentials.overview.head.next_page"), '#', class: "next-page btn btn-default" + .form-group + input.date-search type="date" min=overview.period.first max=overview.period.last + a.search-date href='#' + span.fa.fa-search + .overview-table + .left + .head + .dates= I18n.t("referentials.overview.head.dates") + .lines= I18n.t("referentials.overview.head.lines") + .lines + - overview.lines.each do |line| + .line + a.number style="background-color: #{line.color.present? ? "##{line.color}" : 'whitesmoke'}" title=line.name + = line.number + .company= line.company&.name + .mode= t("enumerize.transport_mode.#{line.transport_mode}") + .right + .inner + .head + - overview.weeks.each do |week| + .week + .week-span= week.span + .week-number= week.number + .days + - week.days.each do |day| + .day style=day.html_style class=day.html_class + .name= day.short_name + .number= day.number + .lines + - if overview.lines.any? + - overview.lines.each do |line| + .line style=line.html_style class=line.html_class + - line.periods.each do |period| + .period style=period.html_style class=period.html_class + .title=period.title + - else + = replacement_msg t('referential_lines.search_no_results') + + = new_pagination overview, 'pull-right' + +- content_for :javascript do + = javascript_pack_tag 'referential_overview/overview.js' + + javascript: + overview_id = "#{overview.pagination_param_name}"; + + coffee: + $ => + new ReferentialOverview("##{overview_id}") diff --git a/app/views/referentials/new.html.slim b/app/views/referentials/new.html.slim index 2bed9f912..13d58ee71 100644 --- a/app/views/referentials/new.html.slim +++ b/app/views/referentials/new.html.slim @@ -1,3 +1,5 @@ +- breadcrumb :referentials + .page_content .container-fluid .row diff --git a/app/views/referentials/show.html.slim b/app/views/referentials/show.html.slim index 9852fb0a3..289e802d7 100644 --- a/app/views/referentials/show.html.slim +++ b/app/views/referentials/show.html.slim @@ -1,31 +1,16 @@ - breadcrumb @referential - page_header_content_for @referential -- content_for :page_header_actions do - - unless (@referential.archived? || !policy(@referential).edit?) - = link_to(t('actions.edit'), edit_referential_path(@referential), class: 'btn btn-default') - -- content_for :page_header_content do - .row.mb-sm - .col-lg-12.text-right - - @referential.action_links.each do |link| - - if link.is_a?(HTMLElement) - = link.to_html(class: 'btn btn-primary') - - else - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content .page_content .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 - = definition_list t('metadatas'), - { t('activerecord.attributes.referential.status') => @referential.archived? ? "<div class='td-block'><span class='fa fa-archive'></span><span>#{t('activerecord.attributes.referential.archived_at')}</span></div>".html_safe : "<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>#{t('activerecord.attributes.referential.archived_at_null')}</span></div>".html_safe, - @referential.human_attribute_name(:validity_period) => (@referential.validity_period.present? ? t('validity_range', debut: l(@referential.try(:validity_period).try(:begin), format: :short), end: l(@referential.try(:validity_period).try(:end), format: :short)) : '-'), - @referential.human_attribute_name(:organisation) => @referential.organisation.name, - @referential.human_attribute_name(:published_at) => '-' } + - attributes = {} + - attributes[@referential.human_attribute_name(:status)] = @referential.referential_read_only? ? "<div class='td-block'><span class='fa fa-archive'></span><span>#{t('activerecord.attributes.referential.archived_at')}</span></div>".html_safe : "<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>#{t('activerecord.attributes.referential.archived_at_null')}</span></div>".html_safe unless @referential.in_referential_suite? + - attributes[@referential.human_attribute_name(:validity_period)] = (@referential.validity_period.present? ? t('validity_range', debut: l(@referential.try(:validity_period).try(:begin), format: :short), end: l(@referential.try(:validity_period).try(:end), format: :short)) : '-') + - attributes[@referential.human_attribute_name(:organisation)] = @referential.organisation.name + - attributes[@referential.human_attribute_name(:merged_at)] = @referential.merged_at ? l(@referential.merged_at, format: :short) : '-' unless @referential.in_referential_suite? + = definition_list t('metadatas'), attributes - if params[:q].present? or @reflines.any? .row @@ -68,11 +53,11 @@ ), \ TableBuilderHelper::Column.new( \ key: 'companies.name', \ - attribute: Proc.new { |n| n.try(:company).try(:name) } \ + attribute: Proc.new { |n| n&.company&.name || "-" } \ ) \ ], - links: [:show], - cls: 'table has-filter has-search' + cls: 'table has-filter has-search', + action: :index = new_pagination @reflines, 'pull-right' @@ -82,6 +67,8 @@ = replacement_msg t('referential_lines.search_no_results') + = referential_overview resource + / Modal(s) = modalbox 'purgeModal' do = simple_form_for [@referential, CleanUp.new] do |f| @@ -102,5 +89,5 @@ .modal-footer button.btn.btn-link type='button' data-dismiss='modal' #{t('cancel')} - - unless policy(@referential).archived? + - unless policy(@referential).referential_read_only? = f.button :submit, t('actions.clean_up') , class: 'btn btn-primary' diff --git a/app/views/routes/_form.html.slim b/app/views/routes/_form.html.slim index 29e5be3d2..81f719437 100644 --- a/app/views/routes/_form.html.slim +++ b/app/views/routes/_form.html.slim @@ -27,7 +27,7 @@ // Get JSON data for route stop points = javascript_tag do | window.itinerary_stop = "#{URI.escape(route_json_for_edit(@route))}"; - | window.I18n = #{(I18n.backend.send(:translations)[I18n.locale].to_json).html_safe}; + // | window.I18n = #{(I18n.backend.send(:translations)[I18n.locale].to_json).html_safe}; / StopPoints Reactux component = javascript_pack_tag 'routes/edit.js' diff --git a/app/views/routes/show.html.slim b/app/views/routes/show.html.slim index 3adf3e2f6..375d7c57b 100644 --- a/app/views/routes/show.html.slim +++ b/app/views/routes/show.html.slim @@ -1,18 +1,5 @@ - breadcrumb :route, @referential, @route - page_header_content_for @route -- content_for :page_header_actions do - - if policy(@route).edit? - = link_to(t('actions.edit'), edit_referential_line_route_path(@referential, @line, @route), class: 'btn btn-default') - -- content_for :page_header_content do - .row.mb-sm - .col-lg-12.text-right - - @route.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content .page_content .container-fluid @@ -22,7 +9,8 @@ { t('objectid') => @route.get_objectid.short_id, t('activerecord.attributes.route.published_name') => (@route.published_name ? @route.published_name : '-'), @route.human_attribute_name(:wayback) => (@route.wayback ? @route.wayback_text : '-' ), - @route.human_attribute_name(:opposite_route) => (@route.opposite_route ? @route.opposite_route.name : '-') } + @route.human_attribute_name(:opposite_route) => (@route.opposite_route ? @route.opposite_route.name : '-'), + @route.human_attribute_name(:checksum) => @route.checksum } - if @route_sp.any? .col-lg-6.col-md-6.col-sm-12.col-xs-12 @@ -40,7 +28,7 @@ ), \ TableBuilderHelper::Column.new( \ key: :name, \ - attribute: Proc.new {|s| s.try(:stop_area).try(:name)}, \ + attribute: Proc.new { |s| content_tag :span, s.stop_area&.name, class: s.stop_area&.area_type }, \ link_to: lambda do |stop_point| \ referential_stop_area_path(@referential, stop_point.stop_area) \ end \ @@ -66,9 +54,9 @@ attribute: Proc.new { |s| t("stop_points.stop_point.for_alighting.#{s.for_alighting}") } \ ) \ ], - links: [:show], sortable: false, - cls: 'table has-stoppoints' + cls: 'table has-stoppoints', + action: :index - else = replacement_msg t('stop_areas.search_no_results') diff --git a/app/views/routing_constraint_zones/_filters.html.slim b/app/views/routing_constraint_zones/_filters.html.slim index 433dde4ab..74e299a8b 100644 --- a/app/views/routing_constraint_zones/_filters.html.slim +++ b/app/views/routing_constraint_zones/_filters.html.slim @@ -1,13 +1,13 @@ = search_form_for @q, url: referential_line_routing_constraint_zones_path(@referential, @line), class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_objectid_cont) = f.search_field :name_or_objectid_cont, class: 'form-control', placeholder: "Indiquez un nom d'ITL ou un ID..." span.input-group-btn button.btn.btn-default#search-btn type='submit' span.fa.fa-search .ffg-row - .form-group + .form-group class=filter_item_class(params[:q], :route_id_eq) = f.label 'Itinéraire associé', required: false, class: 'control-label' = f.input :route_id_eq, as: :select, collection: @line.routing_constraint_zones.pluck(:route_id).uniq, label: false, label_method: lambda { |r| @line.routing_constraint_zones.find_by(route_id: r).route_name }, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez un itinéraire...' }, wrapper_html: { class: 'select2ed'} diff --git a/app/views/routing_constraint_zones/index.html.slim b/app/views/routing_constraint_zones/index.html.slim index 7c54fca68..2f67b467e 100644 --- a/app/views/routing_constraint_zones/index.html.slim +++ b/app/views/routing_constraint_zones/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :routing_constraint_zones, @referential, @line -- content_for :page_header_actions do - - if (policy(Chouette::RoutingConstraintZone).create? && @referential.organisation == current_organisation) - = link_to(t('actions.new'), new_referential_line_routing_constraint_zone_path(@referential, @line), class: 'btn btn-primary') .page_content .container-fluid diff --git a/app/views/routing_constraint_zones/show.html.slim b/app/views/routing_constraint_zones/show.html.slim index 6235ade68..8c8e9b17a 100644 --- a/app/views/routing_constraint_zones/show.html.slim +++ b/app/views/routing_constraint_zones/show.html.slim @@ -1,14 +1,5 @@ - breadcrumb :routing_constraint_zone, @referential, @line, @routing_constraint_zone - page_header_content_for @routing_constraint_zone -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @routing_constraint_zone.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content .page_content .container-fluid @@ -17,7 +8,8 @@ = definition_list t('metadatas'), { @routing_constraint_zone.human_attribute_name(:name) => @routing_constraint_zone.try(:name), @routing_constraint_zone.human_attribute_name(:route) => link_to(@routing_constraint_zone.try(:route_name), [@referential, @line, @routing_constraint_zone.route]), - @routing_constraint_zone.human_attribute_name(:line) => link_to(@line.name, [@referential, @line])} + @routing_constraint_zone.human_attribute_name(:line) => link_to(@line.name, [@referential, @line]), + @routing_constraint_zone.human_attribute_name(:checksum) => @routing_constraint_zone.checksum} .row .col-lg-12 diff --git a/app/views/shared/_development_toolbar.html.slim b/app/views/shared/_development_toolbar.html.slim new file mode 100644 index 000000000..aafd37885 --- /dev/null +++ b/app/views/shared/_development_toolbar.html.slim @@ -0,0 +1,53 @@ +- if Rails.application.config.development_toolbar + = modalbox 'development-toolbar' do + = form_tag development_toolbar_update_settings_path, authenticity_token: true do + .modal-header + h3= "Toolbar" + + .inner + .col.features + h4 + = "Features" + .toggles + = link_to 'all', '#', data: {mask: 'features', val: true} + = link_to 'none', '#', data: {mask: 'features', val: false} + ul + - Rails.application.config.development_toolbar.available_features.sort.each do |feature| + li + = hidden_field_tag "features[#{feature}]", false, id: "" + = check_box_tag "features[#{feature}]", true, has_feature?(feature) + = label :features, feature + - if Rails.application.config.development_toolbar.features_doc_url + = link_to "#{Rails.application.config.development_toolbar.features_doc_url}##{feature}", target: :blank do + .fa.fa-question-circle + .col.permissions + h4 + = "Permissions" + .toggles + = link_to 'all', '#', data: {mask: 'permissions', val: true} + = link_to 'none', '#', data: {mask: 'permissions', val: false} + - model = "" + - Rails.application.config.development_toolbar.available_permissions.sort.each do |permission| + - if permission.split('.').first != model + - model = permission.split('.').first + </ul> + h5 + = model + .toggles + = link_to 'all', '#', data: {mask: "permissions[#{model}", val: true} + = link_to 'none', '#', data: {mask: "permissions[#{model}", val: false} + <ul> + li + = hidden_field_tag "permissions[#{permission}]", false, id: "" + = check_box_tag "permissions[#{permission}]", true, current_user.has_permission?(permission) + = label :permissions, permission, permission.split('.').last + .modal-footer + button.btn.btn-link type='button' data-dismiss='modal' #{t('cancel')} + = submit_tag t("actions.submit"), class: 'btn btn-primary' + + - content_for :javascript do + coffee: + $('#development-toolbar .toggles a').click (e)-> + $('#development-toolbar').find("[name^=\"#{e.currentTarget.dataset.mask}\"]").attr "checked", e.currentTarget.dataset.val == 'true' + e.preventDefault() + false diff --git a/app/views/stif/dashboards/_dashboard.html.slim b/app/views/stif/dashboards/_dashboard.html.slim index f3cd01f46..c28696a94 100644 --- a/app/views/stif/dashboards/_dashboard.html.slim +++ b/app/views/stif/dashboards/_dashboard.html.slim @@ -56,17 +56,16 @@ .panel.panel-default .panel-heading h3.panel-title.with_actions - div - = t('.calendars') - span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present? + = I18n.t("calendars.index.title") + span.badge.ml-xs = @dashboard.calendars.count if @dashboard.calendars.present? div - = link_to '', calendars_path, class: ' fa fa-chevron-right pull-right', title: t('.see') + = link_to '', workgroup_calendars_path(current_workgroup), class: ' fa fa-chevron-right pull-right', title: t('.see') - if @dashboard.calendars.present? .list-group - @dashboard.calendars.first(5).each_with_index do |calendar, i| - = link_to calendar.name, calendar_path(calendar), class: 'list-group-item' if i < 6 + = link_to calendar.name, workgroup_calendar_path(current_workgroup, calendar), class: 'list-group-item' if i < 6 - else .panel-body diff --git a/app/views/stop_area_referentials/show.html.slim b/app/views/stop_area_referentials/show.html.slim index d43333fd9..911006c39 100644 --- a/app/views/stop_area_referentials/show.html.slim +++ b/app/views/stop_area_referentials/show.html.slim @@ -1,13 +1,14 @@ - breadcrumb :stop_area_referential, @stop_area_referential -- content_for :page_header_actions do - = link_to(t('actions.sync'), sync_stop_area_referential_path(@stop_area_referential), method: :post, class: 'btn btn-default') +- if policy(@stop_area_referential).synchronize? + - content_for :page_header_actions do + = link_to(t('actions.sync'), sync_stop_area_referential_path(@stop_area_referential), method: :post, class: 'btn btn-default') - content_for :page_header_content do .row.mb-md .col-lg-12.text-right = link_to stop_area_referential_stop_areas_path(@stop_area_referential), class: 'btn btn-primary' do = Referential.human_attribute_name(:stop_areas) - em.small = " (#{@stop_area_referential.stop_areas.size})" + em.small = " (#{@stop_area_referential.stop_areas.count})" - page_header_content_for @stop_area_referential .page_content diff --git a/app/views/stop_areas/_filters.html.slim b/app/views/stop_areas/_filters.html.slim index 3b99f377c..00369d3ed 100644 --- a/app/views/stop_areas/_filters.html.slim +++ b/app/views/stop_areas/_filters.html.slim @@ -1,18 +1,18 @@ = search_form_for @q, url: stop_area_referential_stop_areas_path(@stop_area_referential), html: {method: :get}, class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_or_objectid_cont) = f.search_field :name_or_objectid_cont, placeholder: t('.name_or_objectid'), class: 'form-control' span.input-group-btn button.btn.btn-default#search-btn type='submit' span.fa.fa-search .ffg-row - = f.input :zip_code_cont, placeholder: t('.zip_code'), label: Chouette::StopArea.human_attribute_name(:zip_code), required: false - = f.input :city_name_cont, placeholder: t('.city_name'), label: Chouette::StopArea.human_attribute_name(:city_name), required: false + = f.input :zip_code_cont, placeholder: t('.zip_code'), label: Chouette::StopArea.human_attribute_name(:zip_code), required: false, wrapper_html: {class: filter_item_class(params[:q], :zip_code_cont)} + = f.input :city_name_cont, placeholder: t('.city_name'), label: Chouette::StopArea.human_attribute_name(:city_name), required: false, wrapper_html: {class: filter_item_class(params[:q], :city_name_cont)} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :area_type_eq_any) = f.label Chouette::StopArea.human_attribute_name(:area_type), required: false, class: 'control-label' - = f.input :area_type_eq_any, collection: Chouette::StopArea.area_type.options.sort, as: :check_boxes, label: false, label_method: lambda{|w| ("<span>" + t("enumerize.stop_area.area_type.#{w[1]}") + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } + = f.input :area_type_eq_any, checked: params[:q] && params[:q][:area_type_eq_any], collection: Chouette::AreaType.options, as: :check_boxes, label: false, label_method: lambda{|w| ("<span>" + w[0] + "</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } .actions = link_to 'Effacer', @workbench, class: 'btn btn-link' diff --git a/app/views/stop_areas/_form.html.slim b/app/views/stop_areas/_form.html.slim index 20c7c0468..bb1fbe1e9 100644 --- a/app/views/stop_areas/_form.html.slim +++ b/app/views/stop_areas/_form.html.slim @@ -6,7 +6,26 @@ /= @map.to_html = f.input :id, as: :hidden = f.input :name, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.name")} - = f.input :area_type, as: :select, :input_html => {:disabled => !@stop_area.new_record?}, :collection => Chouette::StopArea.area_type.options, :include_blank => false + - if has_feature?(:stop_area_localized_names) + .form-group + .col-sm-3.col-xs-5 + .col-sm-9.col-xs-7 + - f.object.localized_names.each do |k, v| + .col-md-6= f.input "localized_names[#{k}]", input_html: {value: v}, label: label_for_country(k) + + = f.input :kind, + as: :radio_buttons, + checked: @stop_area.new_record? ? :commercial : @stop_area.kind, + input_html: { disabled: !@stop_area.new_record? }, + include_blank: false, + item_wrapper_class: 'radio-inline', + wrapper: :horizontal_form, + disabled: !@stop_area.new_record? + .slave data-master="[name='stop_area[kind]']" data-value="commercial" + = f.input :parent_id, as: :select, :collection => [f.object.parent_id], input_html: { data: { select2_ajax: 'true', url: autocomplete_stop_area_referential_stop_areas_path(@stop_area_referential), initvalue: {id: f.object.parent_id, text: f.object.parent.try(:full_name)}}} + - %i(non_commercial commercial).each do |kind| + .slave data-master="[name='stop_area[kind]']" data-value=kind + = f.input :area_type, as: :select, :input_html => {id: kind, :disabled => !@stop_area.new_record?}, :collection => Chouette::AreaType.options(kind), :include_blank => false, disabled: !@stop_area.new_record? .location_info h3 = t("stop_areas.stop_area.localisation") @@ -17,15 +36,18 @@ - unless @stop_area.projection.blank? or @stop_area.projection_type_label.empty? = f.input :projection_xy, :label => t("activerecord.attributes.stop_area.projection_xy", :projection => @referential.projection_type_label), :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.projection_xy")} - = f.input :coordinates, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.coordinates")}, required: true + = f.input :coordinates, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.coordinates")} = f.input :street_name - /= f.input :country_code, required: format_restriction_for_locales(@referential) == '.hub' = f.input :zip_code, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.zip_code")} = f.input :city_name, required: format_restriction_for_locales(@referential) == '.hub', :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.city_name")} + = f.input :country_code, as: :country, priority: ['FR', 'GB', 'DE', 'ES'], :include_blank => true .stop_areas.stop_area.general_info h3 = t("stop_areas.stop_area.general") + - if has_feature?(:stop_area_waiting_time) + = f.input :waiting_time, input_html: { min: 0 } + = f.input :registration_number, required: format_restriction_for_locales(@referential) == '.hub', :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.registration_number")} = f.input :fare_code = f.input :nearest_topic_name, :input_html => {:title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.stop_area.nearest_topic_name")} @@ -43,3 +65,5 @@ .separator = f.button :submit, t('actions.submit'), class: 'btn btn-default formSubmitr', form: 'stop_area_form' + += javascript_pack_tag "stop_areas/new" diff --git a/app/views/stop_areas/autocomplete.rabl b/app/views/stop_areas/autocomplete.rabl new file mode 100644 index 000000000..a5f0bd5ec --- /dev/null +++ b/app/views/stop_areas/autocomplete.rabl @@ -0,0 +1,24 @@ +collection @stop_areas + +node do |stop_area| + { + :id => stop_area.id, + :registration_number => stop_area.registration_number || "", + :short_registration_number => truncate(stop_area.registration_number, :length => 10) || "", + :name => stop_area.name || "", + :short_name => truncate(stop_area.name, :length => 30) || "", + :zip_code => stop_area.zip_code || "", + :city_name => stop_area.city_name || "", + :short_city_name => truncate(stop_area.city_name, :length => 15) || "", + :user_objectid => stop_area.user_objectid, + :longitude => stop_area.longitude, + :latitude => stop_area.latitude, + :area_type => stop_area.area_type, + :comment => stop_area.comment, + :text => "<span class='small label label-info'>#{I18n.t("area_types.label.#{stop_area.area_type}")}</span>#{stop_area.full_name}" + } +end + +node(:stop_area_path) { |stop_area| + stop_area_picture_url(stop_area) || "" +} diff --git a/app/views/stop_areas/index.html.slim b/app/views/stop_areas/index.html.slim index c4d880081..71c7f995c 100644 --- a/app/views/stop_areas/index.html.slim +++ b/app/views/stop_areas/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :stop_areas, @stop_area_referential -- content_for :page_header_actions do - - if policy(Chouette::StopArea).create? - = link_to(t('stop_areas.actions.new'), new_stop_area_referential_stop_area_path(@stop_area_referential), class: 'btn btn-primary') .page_content .container-fluid @@ -24,7 +21,7 @@ key: :name, \ attribute: 'name', \ link_to: lambda do |stop_area| \ - referential_stop_area_path( \ + stop_area_referential_stop_area_path( \ @stop_area_referential, \ stop_area \ ) \ @@ -48,10 +45,9 @@ ), \ TableBuilderHelper::Column.new( \ key: :area_type, \ - attribute: Proc.new { |s| (s.area_type.nil? ? '-' : t("enumerize.stop_area.area_type.#{s.try(:area_type)}")) } \ + attribute: Proc.new { |s| Chouette::AreaType.find(s.area_type).try :label } \ ), \ ], - links: [:show, :edit, :delete], cls: 'table has-filter has-search' = new_pagination @stop_areas, 'pull-right' diff --git a/app/views/stop_areas/show.html.slim b/app/views/stop_areas/show.html.slim index af673bb25..a4e14a272 100644 --- a/app/views/stop_areas/show.html.slim +++ b/app/views/stop_areas/show.html.slim @@ -1,26 +1,26 @@ - breadcrumb :stop_area, @stop_area_referential, @stop_area - page_header_content_for @stop_area -- content_for :page_header_content do - .row - .col-lg-12.text-right.mb-sm - - @stop_area.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content / PageContent .page_content .container-fluid .row .col-lg-6.col-md-6.col-sm-12.col-xs-12 - = definition_list t('metadatas'), - { t('id_reflex') => @stop_area.get_objectid.short_id, - @stop_area.human_attribute_name(:stop_area_type) => t("area_types.label.#{@stop_area.stop_area_type}"), + - attributes = { t('id_reflex') => @stop_area.get_objectid.short_id } + + - if has_feature?(:stop_area_localized_names) + - @stop_area.localized_names.each do |k, v| + - attributes.merge!({label_for_country(k, @stop_area.human_attribute_name(:name)) => v }) if v.present? + - attributes.merge!({ @stop_area.human_attribute_name(:parent) => @stop_area.parent ? link_to(@stop_area.parent.name, stop_area_referential_stop_area_path(@stop_area_referential, @stop_area.parent)) : "-" }) if @stop_area.commercial? + - attributes.merge!({ @stop_area.human_attribute_name(:stop_area_type) => Chouette::AreaType.find(@stop_area.area_type).try(:label), @stop_area.human_attribute_name(:registration_number) => @stop_area.registration_number, - 'Coordonnées' => geo_data(@stop_area, @stop_area_referential), + }) + - attributes.merge!(@stop_area.human_attribute_name(:waiting_time) => @stop_area.waiting_time_text) if has_feature?(:stop_area_waiting_time) + - attributes.merge!({ "Coordonnées" => geo_data(@stop_area, @stop_area_referential), @stop_area.human_attribute_name(:zip_code) => @stop_area.zip_code, @stop_area.human_attribute_name(:city_name) => @stop_area.city_name, + @stop_area.human_attribute_name(:country_code) => @stop_area.country_code.presence || '-', 'Etat' => (@stop_area.deleted_at ? 'Supprimé' : 'Actif'), - @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment) } + @stop_area.human_attribute_name(:comment) => @stop_area.try(:comment), + }) + = definition_list t('metadatas'), attributes diff --git a/app/views/stop_points/_stop_point.html.slim b/app/views/stop_points/_stop_point.html.slim index ca86e339a..e54158cef 100644 --- a/app/views/stop_points/_stop_point.html.slim +++ b/app/views/stop_points/_stop_point.html.slim @@ -5,7 +5,7 @@ = link_to [@referential, stop_point.stop_area], class: "preview", title: "#{Chouette::StopArea.model_name.human.capitalize} #{stop_point.stop_area.name}" do span.name span.label.label-primary = stop_point.position + 1 - = image_tag "map/" + stop_point.stop_area.stop_area_type + ".png" + = image_tag "map/" + stop_point.stop_area.area_type + ".png" = truncate(stop_point.stop_area.name, length: 20) .panel-body @@ -27,4 +27,4 @@ = t(".no_object") - else - stop_point.stop_area.lines.each do |line| - span.label.label-default.line = line.number || truncate( line.name, length: 4 )
\ No newline at end of file + span.label.label-default.line = line.number || truncate( line.name, length: 4 ) diff --git a/app/views/time_tables/_filter.html.slim b/app/views/time_tables/_filter.html.slim index 11e9987c4..030036a13 100644 --- a/app/views/time_tables/_filter.html.slim +++ b/app/views/time_tables/_filter.html.slim @@ -1,17 +1,17 @@ = search_form_for @q, url: referential_time_tables_path(@referential), html: { method: :get, class: 'form form-filter' } do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :comment_cont) = f.text_field :comment_cont, :placeholder => "#{t('time_tables.index.comment')}", class: 'form-control' span.input-group-btn button.btn.btn-default type='submit' span.fa.fa-search .ffg-row - .form-group + .form-group class=filter_item_class(params[:q], :tag_search) = f.label Chouette::TimeTable.human_attribute_name(:tag_search), required: false, class: 'control-label' = f.input :tag_search, as: :tags, collection: Chouette::TimeTable.tags_on(:tags).pluck(:name), label: false, input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': 'Indiquez une étiquette...' }, wrapper_html: { class: 'select2ed'}, include_blank: false, selected: params[:q] ? params[:q]['tag_search'] : nil - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :bounding_dates) = f.label Chouette::TimeTable.human_attribute_name(:bounding_dates), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :bounding_dates do |p| diff --git a/app/views/time_tables/_form.html.slim b/app/views/time_tables/_form.html.slim index d06fdf444..007044e65 100644 --- a/app/views/time_tables/_form.html.slim +++ b/app/views/time_tables/_form.html.slim @@ -5,7 +5,7 @@ = form.input :comment, :input_html => { :title => t("formtastic.titles#{format_restriction_for_locales(@referential)}.time_table.comment")} - if @time_table.new_record? && !@time_table.created_from - = form.input :calendar_id, as: :select, input_html: { class: 'tt_target', style: "width: 100%", data: { 'select2-ajax': 'true', 'select2ed-placeholder': 'Indiquez un modèle de calendrier...', term: 'name_cont', url: autocomplete_calendars_path}} + = form.input :calendar_id, as: :select, input_html: { class: 'tt_target', style: "width: 100%", data: { 'select2-ajax': 'true', 'select2ed-placeholder': 'Indiquez un modèle de calendrier...', term: 'name_cont', url: autocomplete_workgroup_calendars_path(current_workgroup)}} - if @time_table.created_from = form.input :created_from, disabled: true, input_html: { value: @time_table.created_from.comment } diff --git a/app/views/time_tables/_show_time_table.html.slim b/app/views/time_tables/_show_time_table.html.slim index ebfe9d283..102dbfad7 100644 --- a/app/views/time_tables/_show_time_table.html.slim +++ b/app/views/time_tables/_show_time_table.html.slim @@ -2,24 +2,10 @@ - (1..12).each do |month| .col-lg-3.col-md-4.col-sm-4.col-xs-6 = new_alt_calendar(year: @year, month: month, first_day_of_week: 1, calendar_title: "#{I18n.t("date.month_names")[month]}", show_today: false) do |d| - / - if @time_table.excluded_date?(d) - / - [link_to(d.mday, edit_referential_time_table_path(@referential, @time_table) ), {class: "day excluded_date"}] - - if @time_table.include_in_overlap_dates?(d) - - [link_to(d.mday, edit_referential_time_table_path(@referential, @time_table) ), {class: "day overlaped_date", title: 'Voir'}] - - elsif @time_table.include_in_dates?(d) - - [link_to(d.mday, edit_referential_time_table_path(@referential, @time_table) ), {class: "day selected_date", title: 'Voir'}] - - elsif @time_table.include_in_periods?(d) - - [link_to(d.mday, edit_referential_time_table_path(@referential, @time_table) ), {class: "day selected_period", title: 'Voir'}] - -/ .row -/ .col-lg-12 -/ / wip -/ - if @time_table.dates.where("in_out = true").present? -/ h3.time_table_dates = @time_table.human_attribute_name("dates") -/ .dates.content -/ == render "time_tables/dates" -/ -/ - if @time_table.dates.where("in_out = false").present? -/ h3.time_table_dates = @time_table.human_attribute_name("excluded_dates") -/ .excluded_dates.content -/ == render "time_tables/excluded_dates" + - edit_url = [:edit, @referential, time_table].compact + - if time_table.include_in_overlap_dates?(d) + - [link_to(d.mday, edit_url), {class: "day overlaped_date", title: 'Voir'}] + - elsif time_table.include_in_dates?(d) + - [link_to(d.mday, edit_url), {class: "day selected_date", title: 'Voir'}] + - elsif time_table.include_in_periods?(d) + - [link_to(d.mday, edit_url), {class: "day selected_period", title: 'Voir'}] diff --git a/app/views/time_tables/edit.html.slim b/app/views/time_tables/edit.html.slim index e1c566ff4..d8cffb1b0 100644 --- a/app/views/time_tables/edit.html.slim +++ b/app/views/time_tables/edit.html.slim @@ -8,6 +8,6 @@ = javascript_tag do | window.actionType = "#{raw params[:action]}"; - | window.I18n = #{(I18n.backend.send(:translations)[I18n.locale].to_json).html_safe}; + // | window.I18n = #{(I18n.backend.send(:translations)[I18n.locale].to_json).html_safe}; = javascript_pack_tag 'time_tables/edit.js' diff --git a/app/views/time_tables/index.html.slim b/app/views/time_tables/index.html.slim index b684b0bcb..6913712a0 100644 --- a/app/views/time_tables/index.html.slim +++ b/app/views/time_tables/index.html.slim @@ -1,7 +1,4 @@ - breadcrumb :time_tables, @referential -- content_for :page_header_actions do - - if (policy(Chouette::TimeTable).create? && @referential.organisation == current_organisation) - = link_to(t('actions.add'), new_referential_time_table_path(@referential), class: 'btn btn-default') .page_content .container-fluid @@ -54,7 +51,6 @@ attribute: Proc.new { |tt| l(tt.updated_at, format: :short) } \ ) \ ], - links: [:show, :edit], cls: 'table has-search' = new_pagination @time_tables, 'pull-right' @@ -65,6 +61,6 @@ = replacement_msg t('time_tables.search_no_results') = javascript_tag do - | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; + // | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; = javascript_pack_tag 'date_filters' diff --git a/app/views/time_tables/show.html.slim b/app/views/time_tables/show.html.slim index 036581268..e038b73ca 100644 --- a/app/views/time_tables/show.html.slim +++ b/app/views/time_tables/show.html.slim @@ -4,21 +4,6 @@ - content_for :page_header_title, t('time_tables.show.title', name: @time_table.comment), flush: true -- content_for :page_header_actions do - - if policy(@time_table).edit? - = link_to(t('actions.edit'), edit_referential_time_table_path(@referential, @time_table), class: 'btn btn-default') - -- content_for :page_header_content do - .row.mb-sm - .col-lg-12.text-right - - @time_table.action_links.each do |link| - = link_to link.href, - method: link.method, - data: link.data, - class: 'btn btn-primary' do - = link.content - - .page_content .container-fluid .row @@ -28,7 +13,8 @@ 'Couleur associée' => (@time_table.color.nil? ? '-' : content_tag(:span, '', class: 'fa fa-circle', style: "color:#{@time_table.try(:color)}")), 'Etiquettes' => @time_table.tag_list, 'Modèle de calendrier' => (@time_table.calendar ? link_to(@time_table.calendar.name, @time_table.calendar) : '-'), - "Journées d'application pour les périodes ci-dessous" => %w(monday tuesday wednesday thursday friday saturday sunday).collect{ |d| content_tag(:span, t("calendars.days.#{d}"), class: "label label-default #{@time_table.send(d) ? '' : 'disabled'}") }.join.html_safe } + "Journées d'application pour les périodes ci-dessous" => %w(monday tuesday wednesday thursday friday saturday sunday).collect{ |d| content_tag(:span, t("calendars.days.#{d}"), class: "label label-default #{@time_table.send(d) ? '' : 'disabled'}") }.join.html_safe, + Chouette::TimeTable.human_attribute_name(:checksum) => @time_table.checksum} .row .col-lg-12.mb-sm @@ -38,4 +24,4 @@ = link_to '', referential_time_table_path(@referential, @time_table, year: (@year - 1)), class: 'previous_page' = link_to '', referential_time_table_path(@referential, @time_table, year: (@year + 1)), class: 'next_page' - = render 'show_time_table' + = render 'show_time_table', time_table: @time_table diff --git a/app/views/vehicle_journeys/index.html.slim b/app/views/vehicle_journeys/index.html.slim index 52c1a9728..d53d8b50c 100644 --- a/app/views/vehicle_journeys/index.html.slim +++ b/app/views/vehicle_journeys/index.html.slim @@ -1,5 +1,11 @@ - breadcrumb :vehicle_journeys, @referential, @route - content_for :page_header_title, t('vehicle_journeys.index.title', route: @route.name) +- if @route.opposite_route.present? + - content_for :page_header_content do + .row.mb-sm + .col-lg-12.text-right + = link_to(t('routes.actions.opposite_route_timetable'), [@referential, @route.line, @route.opposite_route, :vehicle_journeys], class: 'btn btn-primary sticky-action') + .page_content .container-fluid @@ -11,7 +17,8 @@ = javascript_tag do | window.route_id = #{params[:route_id]}; | window.stopPoints = #{(@stop_points_list.to_json).html_safe}; - | window.jpOrigin = #{(@jp_origin.to_json).html_safe}; + | window.returnStopPoints = #{(@return_stop_points_list.to_json).html_safe}; + | window.jpOrigin = #{(@jp_origin.present? ? @jp_origin.attributes.update({full_schedule: @jp_origin.full_schedule?}).to_json : "null").html_safe}; | window.jpOriginStopPoints = #{(@jp_origin_stop_points.to_json).html_safe}; | window.transportMode = #{(@transport_mode.to_json).html_safe}; | window.transportSubmode = #{(@transport_submode.to_json).html_safe}; @@ -19,6 +26,14 @@ | window.vehicleJourneysPerPage = #{@ppage}; | window.line_footnotes = #{raw @footnotes}; | window.perms = #{raw @perms}; - | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; + | window.features = #{raw @features}; + | window.all_missions = #{(@all_missions.to_json).html_safe}; + | window.custom_fields = #{(@custom_fields.to_json).html_safe}; + // | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; + +- if has_feature?(:vehicle_journeys_return_route) + = javascript_tag do + | window.returnRouteUrl = "#{(@route.opposite_route && url_for([@referential, @route.line, @route.opposite_route, :vehicle_journeys]) || "").html_safe}"; + = javascript_pack_tag 'vehicle_journeys/index.js' diff --git a/app/views/vehicle_journeys/show.rabl b/app/views/vehicle_journeys/show.rabl index 830dee8bd..546c851a4 100644 --- a/app/views/vehicle_journeys/show.rabl +++ b/app/views/vehicle_journeys/show.rabl @@ -1,6 +1,6 @@ object @vehicle_journey -[:objectid, :published_journey_name, :published_journey_identifier, :company_id].each do |attr| +[:objectid, :published_journey_name, :published_journey_identifier, :company_id, :comment, :checksum, :custom_fields].each do |attr| attributes attr, :unless => lambda { |m| m.send( attr).nil?} end @@ -28,13 +28,22 @@ child(:time_tables, :object_root => false) do |time_tables| end end +if has_feature? :purchase_windows + child(:purchase_windows, :object_root => false) do |purchase_windows| + attributes :id, :objectid, :name, :color + end +end + child :footnotes, :object_root => false do |footnotes| attributes :id, :code, :label end child(:vehicle_journey_at_stops_matrix, :object_root => false) do |vehicle_stops| + attributes :id, :connecting_service_id, :boarding_alighting_possibility node do |vehicle_stop| + node(:dummy) { vehicle_stop.dummy } + node(:area_kind) { vehicle_stop.stop_point.stop_area.kind } node(:stop_area_object_id) do vehicle_stop.stop_point.stop_area.objectid @@ -49,15 +58,11 @@ child(:vehicle_journey_at_stops_matrix, :object_root => false) do |vehicle_stops vehicle_stop.stop_point.stop_area.city_name end - [:id, :connecting_service_id, :boarding_alighting_possibility].map do |att| - node(att) { vehicle_stop.send(att) ? vehicle_stop.send(att) : nil } - end - - [:arrival_time, :departure_time].map do |att| - node(att) do |vs| + [:arrival, :departure].each do |att| + node("#{att}_time") do |vs| { - hour: vs.send(att).try(:strftime, '%H'), - minute: vs.send(att).try(:strftime, '%M') + hour: vs.send("#{att}_local_time").try(:strftime, '%H'), + minute: vs.send("#{att}_local_time").try(:strftime, '%M') } end end diff --git a/app/views/workbench_outputs/show.html.slim b/app/views/workbench_outputs/show.html.slim new file mode 100644 index 000000000..a9e106dbb --- /dev/null +++ b/app/views/workbench_outputs/show.html.slim @@ -0,0 +1,40 @@ +/ PageHeader + +- breadcrumb :workbench_output, @workbench +- content_for :page_header_title, t('.title') +- content_for :page_header_content do + .row.mb-sm + .col-lg-12.text-right + = link_to t('.see_current_output'), referential_path(@workbench.output.current), class: 'btn btn-primary' if @workbench.output&.current + = link_to t('merges.actions.create'), new_workbench_merge_path(@workbench), class: 'btn btn-primary' + +.page_content + .container-fluid + .row + .col-lg-12 + = table_builder_2 @workbench_merges, + [ \ + TableBuilderHelper::Column.new( \ + key: :status, \ + attribute: Proc.new { |n| import_status(n.status) }, \ + ), \ + TableBuilderHelper::Column.new( \ + key: :name, \ + attribute: 'full_names', \ + link_to: lambda do |merge| \ + workbench_merge_path merge.workbench, merge \ + end \ + ), \ + TableBuilderHelper::Column.new( \ + name: t(".table_headers.ended_at"), \ + attribute: Proc.new { |n| l(n.ended_at, format: :long) if n.ended_at }, \ + ), \ + TableBuilderHelper::Column.new( \ + key: :creator, \ + attribute: 'creator' \ + ) \ + ], + links: [], + cls: 'table has-search' + + = new_pagination @workbench_merges, 'pull-right' diff --git a/app/views/workbenches/_filters.html.slim b/app/views/workbenches/_filters.html.slim index 4d9e0066c..c9dd13d96 100644 --- a/app/views/workbenches/_filters.html.slim +++ b/app/views/workbenches/_filters.html.slim @@ -1,27 +1,33 @@ = search_form_for @q_for_form, url: workbench_path(@workbench.id), builder: SimpleForm::FormBuilder, class: 'form form-filter' do |f| .ffg-row - .input-group.search_bar + .input-group.search_bar class=filter_item_class(params[:q], :name_cont) = f.search_field :name_cont, class: 'form-control', placeholder: t('referentials.filters.name') span.input-group-btn button.btn.btn-default type='submit' span.fa.fa-search .ffg-row - .form-group + .form-group class=filter_item_class(params[:q], :associated_lines_id_eq) = f.label t('activerecord.models.line.one').upcase, required: false, class: 'control-label' = f.input :associated_lines_id_eq, as: :select, collection: @workbench.lines.includes(:company).order(:name), input_html: { 'data-select2ed': 'true', 'data-select2ed-placeholder': t('referentials.filters.line') }, label: false, label_method: :display_name, wrapper_html: { class: 'select2ed'} - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :archived_at_not_null) = f.label Referential.human_attribute_name(:status), required: false, class: 'control-label' .form-group.checkbox_list = f.input :archived_at_not_null, label: ("<span>#{t('activerecord.attributes.referential.archived_at')}<span class='fa fa-archive'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } = f.input :archived_at_null, label: ("<span>#{t('activerecord.attributes.referential.archived_at_null')}<span class='sb sb-lg sb-preparing'></span></span>").html_safe, as: :boolean, wrapper_html: { class: 'checkbox-wrapper' } - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :organisation_name_eq_any) = f.label t('activerecord.models.organisation.one'), required: false, class: 'control-label' - = f.input :organisation_name_eq_any, collection: Organisation.order('name').pluck(:name), as: :check_boxes, label: false, label_method: lambda{|w| ("<span>#{w}</span>").html_safe}, required: false, wrapper_html: { class: 'checkbox_list' } + = f.input :organisation_name_eq_any, + collection: @workbench.workgroup.organisations.order('name').pluck(:name), + as: :check_boxes, + label: false, + label_method: lambda { |w| ("<span>#{w}</span>").html_safe }, + required: false, + wrapper_html: { class: 'checkbox_list' } - .form-group.togglable + .form-group.togglable class=filter_item_class(params[:q], :validity_period) = f.label Referential.human_attribute_name(:validity_period), required: false, class: 'control-label' .filter_menu = f.simple_fields_for :validity_period do |p| diff --git a/app/views/workbenches/show.html.slim b/app/views/workbenches/show.html.slim index af312fc08..aae34c51b 100644 --- a/app/views/workbenches/show.html.slim +++ b/app/views/workbenches/show.html.slim @@ -6,6 +6,7 @@ - if policy(Referential).create? = link_to t('actions.import'), workbench_imports_path(@workbench), class: 'btn btn-primary' = link_to t('actions.add'), new_referential_path(workbench_id: @workbench), class: 'btn btn-primary' + = link_to t('workbenches.actions.show_output'), workbench_output_path(@workbench), class: 'btn btn-primary' .page_content .container-fluid @@ -29,7 +30,7 @@ ), \ TableBuilderHelper::Column.new( \ key: :status, \ - attribute: Proc.new {|w| w.archived? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>#{t('activerecord.attributes.referential.archived_at')}</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>#{t('activerecord.attributes.referential.archived_at_null')}</span></div>").html_safe} \ + attribute: Proc.new {|w| w.referential_read_only? ? ("<div class='td-block'><span class='fa fa-archive'></span><span>#{t('activerecord.attributes.referential.archived_at')}</span></div>").html_safe : ("<div class='td-block'><span class='sb sb-lg sb-preparing'></span><span>#{t('activerecord.attributes.referential.archived_at_null')}</span></div>").html_safe} \ ), \ TableBuilderHelper::Column.new( \ key: :organisation, \ @@ -53,13 +54,13 @@ attribute: Proc.new {|w| l(w.updated_at, format: :short)} \ ), \ TableBuilderHelper::Column.new( \ - key: :published_at, \ - attribute: '' \ + key: :merged_at, \ + attribute: Proc.new {|w| w.merged_at ? l(w.merged_at, format: :short) : '-'} \ ) \ ], - selectable: true, - links: [:show, :edit], - cls: 'table has-filter has-search' + selectable: ->(ref){ @workbench.referentials.include?(ref) }, + cls: 'table has-filter has-search', + action: :index = multiple_selection_toolbox([:delete], collection_name: 'referentials') @@ -71,6 +72,6 @@ = replacement_msg t('referentials.search_no_results') = javascript_tag do - | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; + // | window.I18n = #{(I18n.backend.send(:translations).to_json).html_safe}; = javascript_pack_tag 'date_filters' diff --git a/app/workers/clean_up_worker.rb b/app/workers/clean_up_worker.rb index 2d76b3a68..9a7c3aa5a 100644 --- a/app/workers/clean_up_worker.rb +++ b/app/workers/clean_up_worker.rb @@ -1,6 +1,5 @@ class CleanUpWorker include Sidekiq::Worker - sidekiq_options :retry => false def perform(id) cleaner = CleanUp.find id diff --git a/app/workers/line_referential_sync_worker.rb b/app/workers/line_referential_sync_worker.rb index 253b8a53c..1303a63fd 100644 --- a/app/workers/line_referential_sync_worker.rb +++ b/app/workers/line_referential_sync_worker.rb @@ -1,6 +1,6 @@ class LineReferentialSyncWorker include Sidekiq::Worker - sidekiq_options :retry => false + sidekiq_options retry: true def process_time Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb new file mode 100644 index 000000000..8a085a25a --- /dev/null +++ b/app/workers/merge_worker.rb @@ -0,0 +1,7 @@ +class MergeWorker + include Sidekiq::Worker + + def perform(id) + Merge.find(id).merge! + end +end diff --git a/app/workers/referential_cloning_worker.rb b/app/workers/referential_cloning_worker.rb index 6592160ec..e24baa90c 100644 --- a/app/workers/referential_cloning_worker.rb +++ b/app/workers/referential_cloning_worker.rb @@ -1,32 +1,7 @@ class ReferentialCloningWorker include Sidekiq::Worker - # Replace default apartment created schema with clone schema from source referential def perform(id) - ref_cloning = ReferentialCloning.find id - - source_schema = ref_cloning.source_referential.slug - target_schema = ref_cloning.target_referential.slug - - clone_schema ref_cloning, source_schema, target_schema - end - - private - - def clone_schema ref_cloning, source_schema, target_schema - ref_cloning.run! - - AF83::SchemaCloner - .new(source_schema, target_schema) - .clone_schema - - ref_cloning.successful! - rescue Exception => e - Rails.logger.error "ReferentialCloningWorker : #{e}" - ref_cloning.failed! - end - - def execute_sql sql - ActiveRecord::Base.connection.execute sql + ReferentialCloning.find(id).clone_with_status! end end diff --git a/app/workers/stop_area_referential_sync_worker.rb b/app/workers/stop_area_referential_sync_worker.rb index 08bcf4f5f..3de351a91 100644 --- a/app/workers/stop_area_referential_sync_worker.rb +++ b/app/workers/stop_area_referential_sync_worker.rb @@ -1,6 +1,6 @@ class StopAreaReferentialSyncWorker include Sidekiq::Worker - sidekiq_options :retry => false + sidekiq_options retry: true def process_time Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) diff --git a/app/workers/workbench_import_worker.rb b/app/workers/workbench_import_worker.rb index de51efded..6420be835 100644 --- a/app/workers/workbench_import_worker.rb +++ b/app/workers/workbench_import_worker.rb @@ -3,29 +3,25 @@ class WorkbenchImportWorker include Rails.application.routes.url_helpers include Configurable + include ObjectStateUpdater + + attr_reader :entries, :workbench_import + # Workers # ======= def perform(import_id) - @workbench_import = WorkbenchImport.find(import_id) - @response = nil - @workbench_import.update(status: 'running', started_at: Time.now) - downloaded = download - zip_service = ZipService.new(downloaded) + @entries = 0 + @workbench_import ||= WorkbenchImport.find(import_id) + + workbench_import.update(status: 'running', started_at: Time.now) + zip_service = ZipService.new(downloaded, allowed_lines) upload zip_service - @workbench_import.update(ended_at: Time.now) + workbench_import.update(ended_at: Time.now) rescue Zip::Error handle_corrupt_zip_file end - def download - logger.info "HTTP GET #{import_url}" - HTTPService.get_resource( - host: import_host, - path: import_path, - params: {token: @workbench_import.token_download}).body - end - def execute_post eg_name, eg_file logger.info "HTTP POST #{export_url} (for #{complete_entry_group_name(eg_name)})" HTTPService.post_resource( @@ -35,48 +31,43 @@ class WorkbenchImportWorker end def handle_corrupt_zip_file - @workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {source_filename: @workbench_import.file.file.file}) + workbench_import.messages.create(criticity: :error, message_key: 'corrupt_zip_file', message_attributes: {source_filename: workbench_import.file.file.file}) end def upload zip_service entry_group_streams = zip_service.subdirs - @workbench_import.update total_steps: entry_group_streams.size entry_group_streams.each_with_index(&method(:upload_entry_group)) + workbench_import.update total_steps: @entries rescue Exception => e logger.error e.message - @workbench_import.update( current_step: entry_group_streams.size, status: 'failed' ) + workbench_import.update( current_step: @entries, status: 'failed' ) raise end - 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: { - 'source_filename' => @workbench_import.file.file.file, - 'spurious_dirs' => entry.spurious.join(', ') - }) - end - end def upload_entry_group entry, element_count update_object_state entry, element_count.succ + return unless entry.ok? # status = retry_service.execute(&upload_entry_group_proc(entry)) - eg_name = entry.name - eg_stream = entry.stream + upload_entry_group_stream entry.name, entry.stream + end + def upload_entry_group_stream eg_name, eg_stream FileUtils.mkdir_p(Rails.root.join('tmp', 'imports')) - eg_file = File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip"), 'wb').tap do |file| + File.open(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip"), 'wb') do |file| eg_stream.rewind file.write eg_stream.read end - eg_file.close - eg_file = File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip")) + + upload_entry_group_tmpfile eg_name, File.new(Rails.root.join('tmp', 'imports', "WorkbenchImport_#{eg_name}_#{$$}.zip")) + end + + def upload_entry_group_tmpfile eg_name, eg_file result = execute_post eg_name, eg_file if result && result.status < 400 + @entries += 1 + workbench_import.update( current_step: @entries ) result else raise StopIteration, result.body @@ -91,7 +82,7 @@ class WorkbenchImportWorker # ======= def complete_entry_group_name entry_group_name - [@workbench_import.name, entry_group_name].join("--") + [workbench_import.name, entry_group_name].join("--") end # Constants @@ -111,7 +102,7 @@ class WorkbenchImportWorker Rails.application.config.rails_host end def import_path - @__import_path__ ||= download_workbench_import_path(@workbench_import.workbench, @workbench_import) + @__import_path__ ||= download_workbench_import_path(workbench_import.workbench, workbench_import) end def import_url @__import_url__ ||= File.join(import_host, import_path) @@ -119,10 +110,29 @@ class WorkbenchImportWorker def params file, name { netex_import: - { parent_id: @workbench_import.id, - parent_type: @workbench_import.class.name, - workbench_id: @workbench_import.workbench_id, - name: name, - file: HTTPService.upload(file, 'application/zip', "#{name}.zip") } } + { parent_id: workbench_import.id, + parent_type: workbench_import.class.name, + workbench_id: workbench_import.workbench_id, + name: name, + file: HTTPService.upload(file, 'application/zip', "#{name}.zip") } } + end + + # Lazy Values + # =========== + + def allowed_lines + @__allowed_lines__ ||= workbench_import.workbench.organisation.lines_set end + def downloaded + @__downloaded__ ||= download_response.body + end + def download_response + @__download_response__ ||= HTTPService.get_resource( + host: import_host, + path: import_path, + params: {token: workbench_import.token_download}).tap do + logger.info "HTTP GET #{import_url}" + end + end + end diff --git a/app/workers/workbench_import_worker/object_state_updater.rb b/app/workers/workbench_import_worker/object_state_updater.rb new file mode 100644 index 000000000..67bdc0654 --- /dev/null +++ b/app/workers/workbench_import_worker/object_state_updater.rb @@ -0,0 +1,36 @@ + +class WorkbenchImportWorker + module ObjectStateUpdater + + def update_object_state entry, count + workbench_import.update( total_steps: count ) + update_spurious entry + update_foreign_lines entry + end + + + private + + def update_foreign_lines entry + return if entry.foreign_lines.empty? + workbench_import.messages.create( + criticity: :error, + message_key: 'foreign_lines_in_referential', + message_attributes: { + 'source_filename' => workbench_import.file.file.file, + 'foreign_lines' => entry.foreign_lines.join(', ') + }) + end + + def update_spurious entry + return if entry.spurious.empty? + workbench_import.messages.create( + criticity: :error, + message_key: 'inconsistent_zip_file', + message_attributes: { + 'source_filename' => workbench_import.file.file.file, + 'spurious_dirs' => entry.spurious.join(', ') + }) + end + end +end |
