diff options
| -rw-r--r-- | app/models/concerns/rrule_humanizer.rb | 34 | ||||
| -rw-r--r-- | app/views/events/edit.html.erb | 8 | ||||
| -rw-r--r-- | public/javascripts/admin_interface.js | 46 | ||||
| -rw-r--r-- | test/models/concerns/rrule_humanizer_test.rb | 18 |
4 files changed, 98 insertions, 8 deletions
diff --git a/app/models/concerns/rrule_humanizer.rb b/app/models/concerns/rrule_humanizer.rb index 8231de8..ca29648 100644 --- a/app/models/concerns/rrule_humanizer.rb +++ b/app/models/concerns/rrule_humanizer.rb | |||
| @@ -15,8 +15,8 @@ module RruleHumanizer | |||
| 15 | }.freeze | 15 | }.freeze |
| 16 | 16 | ||
| 17 | ORDINAL_NAMES = { | 17 | ORDINAL_NAMES = { |
| 18 | de: { 1=>"ersten", 2=>"zweiten", 3=>"dritten", 4=>"vierten", -1=>"letzten", -2=>"vorletzten" }, | 18 | de: { 1=>"ersten", 2=>"zweiten", 3=>"dritten", 4=>"vierten", 5=>"fünften", -1=>"letzten", -2=>"vorletzten" }, |
| 19 | en: { 1=>"first", 2=>"second", 3=>"third", 4=>"fourth", -1=>"last", -2=>"second-to-last" } | 19 | en: { 1=>"first", 2=>"second", 3=>"third", 4=>"fourth", 5=>"fifth", -1=>"last", -2=>"second-to-last" } |
| 20 | }.freeze | 20 | }.freeze |
| 21 | 21 | ||
| 22 | MONTH_NAMES = { | 22 | MONTH_NAMES = { |
| @@ -35,7 +35,8 @@ module RruleHumanizer | |||
| 35 | ordinals = ORDINAL_NAMES[loc] || ORDINAL_NAMES[:en] | 35 | ordinals = ORDINAL_NAMES[loc] || ORDINAL_NAMES[:en] |
| 36 | months = MONTH_NAMES[loc] || MONTH_NAMES[:en] | 36 | months = MONTH_NAMES[loc] || MONTH_NAMES[:en] |
| 37 | 37 | ||
| 38 | days = byday&.split(",")&.map do |d| | 38 | byday_values = byday&.split(",") |
| 39 | days = byday_values&.map do |d| | ||
| 39 | if d =~ /^(-?\d+)([A-Z]{2})$/ | 40 | if d =~ /^(-?\d+)([A-Z]{2})$/ |
| 40 | "#{ordinals[$1.to_i]} #{weekdays[$2]}" | 41 | "#{ordinals[$1.to_i]} #{weekdays[$2]}" |
| 41 | else | 42 | else |
| @@ -43,6 +44,21 @@ module RruleHumanizer | |||
| 43 | end | 44 | end |
| 44 | end | 45 | end |
| 45 | 46 | ||
| 47 | excluded_monthly_ordinal = nil | ||
| 48 | excluded_monthly_weekday = nil | ||
| 49 | if freq == "MONTHLY" && byday_values.present? | ||
| 50 | ordinal_days = byday_values.map { |d| d.match(/^([1-5])([A-Z]{2})$/) } | ||
| 51 | if ordinal_days.all? | ||
| 52 | positions = ordinal_days.map { |match| match[1].to_i }.uniq.sort | ||
| 53 | weekdays_in_rule = ordinal_days.map { |match| match[2] }.uniq | ||
| 54 | missing_positions = (1..5).to_a - positions | ||
| 55 | if positions.size == 4 && weekdays_in_rule.size == 1 && missing_positions.size == 1 | ||
| 56 | excluded_monthly_ordinal = missing_positions.first | ||
| 57 | excluded_monthly_weekday = weekdays_in_rule.first | ||
| 58 | end | ||
| 59 | end | ||
| 60 | end | ||
| 61 | |||
| 46 | base = | 62 | base = |
| 47 | case loc | 63 | case loc |
| 48 | when :de | 64 | when :de |
| @@ -59,14 +75,22 @@ module RruleHumanizer | |||
| 59 | interval == 2 ? "Alle zwei Wochen" : "Wöchentlich" | 75 | interval == 2 ? "Alle zwei Wochen" : "Wöchentlich" |
| 60 | end | 76 | end |
| 61 | when "MONTHLY" | 77 | when "MONTHLY" |
| 62 | days ? "Jeden #{days.join(' und ')} im Monat" : "Monatlich" | 78 | if excluded_monthly_ordinal |
| 79 | "Jeden #{weekdays[excluded_monthly_weekday]} im Monat, außer dem #{ordinals[excluded_monthly_ordinal]} #{weekdays[excluded_monthly_weekday]}" | ||
| 80 | else | ||
| 81 | days ? "Jeden #{days.join(' und ')} im Monat" : "Monatlich" | ||
| 82 | end | ||
| 63 | end | 83 | end |
| 64 | else | 84 | else |
| 65 | case freq | 85 | case freq |
| 66 | when "WEEKLY" | 86 | when "WEEKLY" |
| 67 | days ? "#{interval == 2 ? 'Every other' : 'Every'} #{days.join(' and ')}" : (interval == 2 ? "Every other week" : "Weekly") | 87 | days ? "#{interval == 2 ? 'Every other' : 'Every'} #{days.join(' and ')}" : (interval == 2 ? "Every other week" : "Weekly") |
| 68 | when "MONTHLY" | 88 | when "MONTHLY" |
| 69 | days ? "Every #{days.join(' and ')} of the month" : "Monthly" | 89 | if excluded_monthly_ordinal |
| 90 | "Every #{weekdays[excluded_monthly_weekday]} of the month, except the #{ordinals[excluded_monthly_ordinal]} #{weekdays[excluded_monthly_weekday]}" | ||
| 91 | else | ||
| 92 | days ? "Every #{days.join(' and ')} of the month" : "Monthly" | ||
| 93 | end | ||
| 70 | end | 94 | end |
| 71 | end | 95 | end |
| 72 | return nil unless base | 96 | return nil unless base |
diff --git a/app/views/events/edit.html.erb b/app/views/events/edit.html.erb index b6564a4..bf8cb35 100644 --- a/app/views/events/edit.html.erb +++ b/app/views/events/edit.html.erb | |||
| @@ -56,9 +56,15 @@ | |||
| 56 | <div id="rrule_monthly_options" style="display: none;"> | 56 | <div id="rrule_monthly_options" style="display: none;"> |
| 57 | <p><label><%= check_box_tag "rrule_monthly_ordinal" %> On a specific weekday each month</label></p> | 57 | <p><label><%= check_box_tag "rrule_monthly_ordinal" %> On a specific weekday each month</label></p> |
| 58 | <p id="rrule_ordinal_fields" style="display: none;"> | 58 | <p id="rrule_ordinal_fields" style="display: none;"> |
| 59 | <%= select_tag "rrule_ordinal", options_for_select([["1.", 1], ["2.", 2], ["3.", 3], ["4.", 4], ["letzter", -1], ["vorletzter", -2]]) %> | 59 | <%= select_tag "rrule_ordinal", options_for_select([["1.", 1], ["2.", 2], ["3.", 3], ["4.", 4], ["letzter", -1], ["vorletzter", -2], ["Selected weeks", "custom"]]) %> |
| 60 | <%= select_tag "rrule_ordinal_day", options_for_select(%w[MO TU WE TH FR SA SU].map { |c| [RruleHumanizer::WEEKDAY_NAMES[:de][c], c] }) %> | 60 | <%= select_tag "rrule_ordinal_day", options_for_select(%w[MO TU WE TH FR SA SU].map { |c| [RruleHumanizer::WEEKDAY_NAMES[:de][c], c] }) %> |
| 61 | </p> | 61 | </p> |
| 62 | <p id="rrule_custom_ordinal_fields" style="display: none;"> | ||
| 63 | Weeks of the month: | ||
| 64 | <% (1..5).each do |ordinal| %> | ||
| 65 | <label><%= check_box_tag "rrule_custom_ordinal_#{ordinal}" %> <%= ordinal %>.</label> | ||
| 66 | <% end %> | ||
| 67 | </p> | ||
| 62 | </div> | 68 | </div> |
| 63 | <p> | 69 | <p> |
| 64 | <label><%= check_box_tag "rrule_exclude_month" %> Except in one month</label> | 70 | <label><%= check_box_tag "rrule_exclude_month" %> Except in one month</label> |
diff --git a/public/javascripts/admin_interface.js b/public/javascripts/admin_interface.js index 9ce0b9e..3988016 100644 --- a/public/javascripts/admin_interface.js +++ b/public/javascripts/admin_interface.js | |||
| @@ -415,8 +415,10 @@ rrule_builder = { | |||
| 415 | }); | 415 | }); |
| 416 | $("#rrule_monthly_ordinal").bind("change", function() { | 416 | $("#rrule_monthly_ordinal").bind("change", function() { |
| 417 | $("#rrule_ordinal_fields").toggle($(this).is(":checked")); | 417 | $("#rrule_ordinal_fields").toggle($(this).is(":checked")); |
| 418 | rrule_builder.toggle_custom_ordinal_fields(); | ||
| 418 | rrule_builder.sync(); | 419 | rrule_builder.sync(); |
| 419 | }); | 420 | }); |
| 421 | $("#rrule_ordinal").bind("change", rrule_builder.toggle_custom_ordinal_fields); | ||
| 420 | $("#rrule_exclude_month").bind("change", function() { | 422 | $("#rrule_exclude_month").bind("change", function() { |
| 421 | $("#rrule_excluded_month").toggle($(this).is(":checked")); | 423 | $("#rrule_excluded_month").toggle($(this).is(":checked")); |
| 422 | rrule_builder.sync(); | 424 | rrule_builder.sync(); |
| @@ -433,6 +435,12 @@ rrule_builder = { | |||
| 433 | $("#rrule_monthly_options").show(); | 435 | $("#rrule_monthly_options").show(); |
| 434 | }, | 436 | }, |
| 435 | 437 | ||
| 438 | toggle_custom_ordinal_fields : function() { | ||
| 439 | $("#rrule_custom_ordinal_fields").toggle( | ||
| 440 | $("#rrule_monthly_ordinal").is(":checked") && $("#rrule_ordinal").val() === "custom" | ||
| 441 | ); | ||
| 442 | }, | ||
| 443 | |||
| 436 | sync : function() { | 444 | sync : function() { |
| 437 | var freq = $("input[name='rrule_freq']:checked").val(); | 445 | var freq = $("input[name='rrule_freq']:checked").val(); |
| 438 | var parts = []; | 446 | var parts = []; |
| @@ -448,7 +456,16 @@ rrule_builder = { | |||
| 448 | } else { | 456 | } else { |
| 449 | parts.push("FREQ=MONTHLY"); | 457 | parts.push("FREQ=MONTHLY"); |
| 450 | if ($("#rrule_monthly_ordinal").is(":checked")) { | 458 | if ($("#rrule_monthly_ordinal").is(":checked")) { |
| 451 | parts.push("BYDAY=" + $("#rrule_ordinal").val() + $("#rrule_ordinal_day").val()); | 459 | var ordinal = $("#rrule_ordinal").val(); |
| 460 | if (ordinal === "custom") { | ||
| 461 | var customDays = []; | ||
| 462 | $("input[id^='rrule_custom_ordinal_']:checked").each(function() { | ||
| 463 | customDays.push($(this).attr("id").replace("rrule_custom_ordinal_", "") + $("#rrule_ordinal_day").val()); | ||
| 464 | }); | ||
| 465 | if (customDays.length > 0) parts.push("BYDAY=" + customDays.join(",")); | ||
| 466 | } else { | ||
| 467 | parts.push("BYDAY=" + ordinal + $("#rrule_ordinal_day").val()); | ||
| 468 | } | ||
| 452 | } | 469 | } |
| 453 | } | 470 | } |
| 454 | 471 | ||
| @@ -483,8 +500,33 @@ rrule_builder = { | |||
| 483 | } else if (parts.FREQ === "MONTHLY") { | 500 | } else if (parts.FREQ === "MONTHLY") { |
| 484 | $("input[name='rrule_freq'][value='monthly']").prop("checked", true); | 501 | $("input[name='rrule_freq'][value='monthly']").prop("checked", true); |
| 485 | rrule_builder.show_monthly_options(); | 502 | rrule_builder.show_monthly_options(); |
| 503 | var ordinalDays = parts.BYDAY && parts.BYDAY.split(","); | ||
| 504 | var customOrdinalDay = null; | ||
| 505 | var customOrdinals = []; | ||
| 506 | var customOrdinalsValid = ordinalDays && ordinalDays.length > 0; | ||
| 507 | if (customOrdinalsValid) { | ||
| 508 | ordinalDays.forEach(function(value) { | ||
| 509 | var customMatch = value.match(/^([1-5])([A-Z]{2})$/); | ||
| 510 | if (!customMatch || (customOrdinalDay && customOrdinalDay !== customMatch[2])) { | ||
| 511 | customOrdinalsValid = false; | ||
| 512 | return; | ||
| 513 | } | ||
| 514 | customOrdinalDay = customMatch[2]; | ||
| 515 | customOrdinals.push(customMatch[1]); | ||
| 516 | }); | ||
| 517 | } | ||
| 518 | |||
| 486 | var match = parts.BYDAY && parts.BYDAY.match(/^(-?\d+)([A-Z]{2})$/); | 519 | var match = parts.BYDAY && parts.BYDAY.match(/^(-?\d+)([A-Z]{2})$/); |
| 487 | if (match) { | 520 | if (customOrdinalsValid && (customOrdinals.length > 1 || customOrdinals[0] === "5")) { |
| 521 | $("#rrule_monthly_ordinal").prop("checked", true); | ||
| 522 | $("#rrule_ordinal_fields").show(); | ||
| 523 | $("#rrule_ordinal").val("custom"); | ||
| 524 | $("#rrule_ordinal_day").val(customOrdinalDay); | ||
| 525 | customOrdinals.forEach(function(ordinal) { | ||
| 526 | $("#rrule_custom_ordinal_" + ordinal).prop("checked", true); | ||
| 527 | }); | ||
| 528 | rrule_builder.toggle_custom_ordinal_fields(); | ||
| 529 | } else if (match) { | ||
| 488 | $("#rrule_monthly_ordinal").prop("checked", true); | 530 | $("#rrule_monthly_ordinal").prop("checked", true); |
| 489 | $("#rrule_ordinal_fields").show(); | 531 | $("#rrule_ordinal_fields").show(); |
| 490 | $("#rrule_ordinal").val(match[1]); | 532 | $("#rrule_ordinal").val(match[1]); |
diff --git a/test/models/concerns/rrule_humanizer_test.rb b/test/models/concerns/rrule_humanizer_test.rb index 279ff73..c4e78d4 100644 --- a/test/models/concerns/rrule_humanizer_test.rb +++ b/test/models/concerns/rrule_humanizer_test.rb | |||
| @@ -47,6 +47,24 @@ class RruleHumanizerTest < ActiveSupport::TestCase | |||
| 47 | assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en) | 47 | assert_equal "Every second-to-last Thursday of the month", humanize("FREQ=MONTHLY;BYDAY=-2TH", :en) |
| 48 | end | 48 | end |
| 49 | 49 | ||
| 50 | test "monthly selected weeks" do | ||
| 51 | assert_equal "Jeden ersten Dienstag und dritten Dienstag im Monat", | ||
| 52 | humanize("FREQ=MONTHLY;BYDAY=1TU,3TU") | ||
| 53 | assert_equal "Every first Tuesday and third Tuesday of the month", | ||
| 54 | humanize("FREQ=MONTHLY;BYDAY=1TU,3TU", :en) | ||
| 55 | end | ||
| 56 | |||
| 57 | test "monthly fifth weekday" do | ||
| 58 | assert_equal "Jeden fünften Dienstag im Monat", humanize("FREQ=MONTHLY;BYDAY=5TU") | ||
| 59 | assert_equal "Every fifth Tuesday of the month", humanize("FREQ=MONTHLY;BYDAY=5TU", :en) | ||
| 60 | end | ||
| 61 | |||
| 62 | test "monthly weekday excluding one ordinal" do | ||
| 63 | rrule = "FREQ=MONTHLY;BYDAY=1TU,3TU,4TU,5TU" | ||
| 64 | assert_equal "Jeden Dienstag im Monat, außer dem zweiten Dienstag", humanize(rrule) | ||
| 65 | assert_equal "Every Tuesday of the month, except the second Tuesday", humanize(rrule, :en) | ||
| 66 | end | ||
| 67 | |||
| 50 | test "monthly no byday" do | 68 | test "monthly no byday" do |
| 51 | assert_equal "Monatlich", humanize("FREQ=MONTHLY") | 69 | assert_equal "Monatlich", humanize("FREQ=MONTHLY") |
| 52 | assert_equal "Monthly", humanize("FREQ=MONTHLY", :en) | 70 | assert_equal "Monthly", humanize("FREQ=MONTHLY", :en) |
