############################################################################ # Copyright 2009-2019 Benjamin Kellermann # # # # This file is part of Dudle. # # # # Dudle is free software: you can redistribute it and/or modify it under # # the terms of the GNU Affero General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # Dudle is distributed in the hope that it will be useful, but WITHOUT ANY # # WARRANTY; without even the implied warranty of MERCHANTABILITY or # # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # # License for more details. # # # # You should have received a copy of the GNU Affero General Public License # # along with dudle. If not, see <http://www.gnu.org/licenses/>. # ############################################################################ # BUGFIX for Time.parse, which handles the zone indeterministically class << Time alias_method :old_parse, :parse def Time.parse(date, now=self.now) Time.old_parse("2009-10-25 00:30") Time.old_parse(date) end end require_relative "timestring" class TimePollHead def initialize @data = [] end def col_size @data.size end # returns a sorted array of all columns # column should be the internal representation # column.to_s should deliver humanreadable form def columns @data.sort.collect{|day| day.to_s} end def concrete_times h = {} @data.each{|ds| h[ds.time_to_s] = true } h.keys end def date_included?(date) ret = false @data.each{|ds| ret = ret || ds.date == date } ret end # deletes one concrete column # adds the empty day, if it was the last concrete time of the day # returns true if deletion successful def delete_concrete_column(col) ret = @data.delete(col) != nil @data << TimeString.new(col.date,nil) unless date_included?(col.date) ret end # column is in human readable form # returns true if deletion successful def delete_column(column) if $cgi.include?("togglealloff") # delete one time head_count("%Y-%m-%d",false).each{|day,num| delete_concrete_column(TimeString.new(day,column)) } return true end col = TimeString.from_s(column) if col.time return delete_concrete_column(col) else # delete all concrete times on the given day deldata = [] @data.each{|ts| deldata << ts if ts.date == col.date } deldata.each{|ts| @data.delete(ts) } return !deldata.empty? end end def parsecolumntitle(title) if $cgi.include?("add_remove_column_day") parsed_date = YAML::load(Time.parse("#{$cgi["add_remove_column_month"]}-#{$cgi["add_remove_column_day"]} #{title}").to_yaml) else earlytime = @head.keys.collect{|t|t.strftime("%H:%M")}.sort[0] parsed_date = YAML::load(Time.parse("#{$cgi["add_remove_column_month"]}-#{title} #{earlytime}").to_yaml) end parsed_date end # returns parsed title or nil in case of column not changed def edit_column(column, newtitle, cgi) if cgi.include?("toggleallon") head_count("%Y-%m-%d",false).each{|day,num| parsed_date = TimeString.new(day,newtitle) delete_column(day) if @data.include?(TimeString.new(day,nil)) @data << parsed_date unless @data.include?(parsed_date) } return newtitle end if cgi.include?("columntime") && cgi["columntime"] == "" @edit_column_error = _("To add a time other than the default times, please enter some string here (e. g., 09:30, morning, afternoon).") return nil end delete_column(column) if column != "" parsed_date = TimeString.new(newtitle, cgi["columntime"] != "" ? cgi["columntime"] : nil) if @data.include?(parsed_date) @edit_column_error = _("This time has already been selected.") return nil else @data << parsed_date parsed_date.to_s end end # returns a sorted array, containing the big units and how often each small is in the big one # small and big must be formatted for strftime # ex: head_count("%Y-%m") returns an array like [["2009-03",2],["2009-04",3]] # if notime = true, the time field is stripped out before counting def head_count(elem, notime) data = @data.collect{|day| day.date} data.uniq! if notime ret = Hash.new(0) data.each{|day| ret[day.strftime(elem)] += 1 } ret.sort end def to_html(scols,config = false,activecolumn = nil) ret = "<tr><th colspan='2' class='invisible'></th>" head_count("%Y-%m",false).each{|title,count| year, month = title.split("-").collect{|e| e.to_i} ret += "<th colspan='#{count}'>#{Date.parse("#{year}-#{month}-01").strftime("%b %Y")}</th>\n" } ret += "<th class='invisible'></th></tr><tr><th colspan='2' class='invisible'></th>" head_count("%Y-%m-%d",false).each{|title,count| ret += "<th colspan='#{count}'>#{Date.parse(title).strftime('%a, %d')}</th>\n" } def sortsymb(scols,col) return <<SORTSYMBOL <span class='sortsymb'> #{scols.include?(col) ? SORT : NOSORT}</span> SORTSYMBOL end ret += "<th class='invisible'></th></tr><tr><th colspan='2'><a href='?sort=name'>" + _("Name") + " #{sortsymb(scols,"name")}</a></th>" @data.sort.each{|date| ret += "<th><a title=\"#{CGI.escapeHTML(date.to_s)}\" href=\"?sort=#{CGI.escape(date.to_s)}\">#{CGI.escapeHTML(date.time_to_s)} #{sortsymb(scols,date.to_s)}</a></th>\n" } ret += "<th><a href='?'>" + _("Last edit") + " #{sortsymb(scols,"timestamp")}</a></th>\n</tr>\n" ret end def datenavi val,revision case val when MONTHBACK navimonth = Date.parse("#{@startdate.strftime('%Y-%m')}-1")-1 when MONTHFORWARD navimonth = Date.parse("#{@startdate.strftime('%Y-%m')}-1")+31 else raise "Unknown navi value #{val}" end return <<END <th colspan='2' style='padding:0px'> <form method='post' action=''> <div> <input class='navigation' type='submit' value='#{val}' /> <input type='hidden' name='add_remove_column_month' value='#{navimonth.strftime("%Y-%m")}' /> <input type='hidden' name='firsttime' value='#{@firsttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='lasttime' value='#{@lasttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='undo_revision' value='#{revision}' /> </div> </form> </th> END end def timenavi val,revision case val when EARLIER return "" if @firsttime == 0 str = EARLIER + " " + _("Earlier") firsttime = [@firsttime-2,0].max lasttime = @lasttime when LATER return "" if @lasttime == 23 str = LATER + " " + _("Later") firsttime = @firsttime lasttime = [@lasttime+2,23].min else raise "Unknown navi value #{val}" end return <<END <tr> <td class='navigation' colspan='2'> <form method='post' action=''> <div> <input class='navigation' type='submit' value='#{str}' /> <input type='hidden' name='firsttime' value='#{firsttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='lasttime' value='#{lasttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='add_remove_column_month' value='#{@startdate.strftime("%Y-%m")}' /> <input type='hidden' name='undo_revision' value='#{revision}' /> </div> </form> </td> </tr> END end def edit_column_htmlform(activecolumn, revision) # calculate start date, first and last time to show if $cgi.include?("add_remove_column_month") @startdate = Date.parse("#{$cgi["add_remove_column_month"]}-1") else @startdate = Date.parse("#{Date.today.year}-#{Date.today.month}-1") end times = concrete_times times.delete("") # do not display empty cell in edit-column-form realtimes = times.collect{|t| begin Time.parse(t) if t =~ /^\d\d:\d\d$/ rescue ArgumentError end }.compact [9,16].each{|i| realtimes << Time.parse("#{i.to_s.rjust(2,"0")}:00")} ["firsttime","lasttime"].each{|t| realtimes << Time.parse($cgi[t]) if $cgi.include?(t) } @firsttime = realtimes.min.strftime("%H").to_i @lasttime = realtimes.max.strftime("%H").to_i def add_remove_button(klasse, buttonlabel, action, columnstring, revision, pretext = "") titlestr = _("Add column") titlestr = _("Delete column") if klasse == "chosen" || klasse == "delete" return <<FORM <form method='post' action=''> <div> #{pretext}<input title='#{titlestr}' class='#{klasse}' type='submit' value="#{buttonlabel}" /> <input type='hidden' name='#{action}' value="#{CGI.escapeHTML(columnstring)}" /> <input type='hidden' name='firsttime' value="#{@firsttime.to_s.rjust(2,"0")}:00" /> <input type='hidden' name='lasttime' value="#{@lasttime.to_s.rjust(2,"0")}:00" /> <input type='hidden' name='add_remove_column_month' value="#{@startdate.strftime("%Y-%m")}" /> <input type='hidden' name='undo_revision' value='#{revision}' /> </div> </form> FORM end hintstr = _("Click on the dates to add or remove columns.") ret = <<END <table style='width:100%'><tr><td style="vertical-align:top"> <div id='AddRemoveColumndaysDescription' class='shorttextcolumn'> #{hintstr} </div> <table border='1' class='calendarday'><tr> END ret += datenavi(MONTHBACK,revision) ret += "<th colspan='3'>#{@startdate.strftime('%b %Y')}</th>" ret += datenavi(MONTHFORWARD,revision) ret += "</tr><tr>\n" 7.times{|i| # 2010-03-01 was a Monday, so we can use this month for a dirty hack ret += "<th class='weekday'>#{Date.parse("2010-03-0#{i+1}").strftime("%a")}</th>" } ret += "</tr><tr>\n" ((@startdate.wday+7-1)%7).times{ ret += "<td class='invisible'></td>" } d = @startdate while true do klasse = "notchosen" varname = "new_columnname" klasse = "disabled" if d < Date.today if date_included?(d) klasse = "chosen" varname = "deletecolumn" end ret += "<td class='calendarday'>#{add_remove_button(klasse, d.day, varname, d.strftime('%Y-%m-%d'),revision)}</td>" d = d.next break if d.month != @startdate.month ret += "</tr><tr>\n" if d.wday == 1 end ret += <<END </tr></table> </td> END ########################### # starting hour input ########################### ret += "<td style='vertical-align:top'>" if col_size > 0 optstr = _("Optional:") hintstr = _("Select specific start times.") ret += <<END <div id='ConcreteColumndaysDescription' class='shorttextcolumn'> #{optstr}<br/> #{hintstr} </div> <table border='1' class='calendarday'> <tr> END ret += "<th class='invisible' colspan='2'></th>" head_count("%Y-%m",true).each{|title,count| year,month = title.split("-").collect{|e| e.to_i} ret += "<th colspan='#{count}'>#{Date.parse("#{year}-#{month}-01").strftime("%b %Y")}</th>\n" } ret += "</tr><tr><th colspan='2'>" + _("Time") + "</th>" head_count("%Y-%m-%d",true).each{|title,count| coltime = Date.parse(title) ret += "<th>" + add_remove_button("delete",DELETE, "deletecolumn", coltime.strftime("%Y-%m-%d"), revision, "#{coltime.strftime('%a, %d')} ") + "</th>" } ret += "</tr>" days = @data.sort.collect{|date| date.date }.uniq chosenstr = { "chosen" => _("Selected"), "notchosen" => _("Not selected"), "disabled" => _("Past") } ret += timenavi(EARLIER,revision) (@firsttime..@lasttime).each{|i| times << "#{i.to_s.rjust(2,"0")}:00" } times.flatten.compact.uniq.sort{|a,b| if a =~ /^\d\d:\d\d$/ && !(b =~ /^\d\d:\d\d$/) -1 elsif !(a =~ /^\d\d:\d\d$/) && b =~ /^\d\d:\d\d$/ 1 else a.to_i == b.to_i ? a <=> b : a.to_i <=> b.to_i end }.each{|time| ret += <<END <tr> <td class='navigation'>#{CGI.escapeHTML(time)}</td> <td class='navigation' style='padding:0px'> <form method='post' action='' accept-charset='utf-8'> <div> <input type='hidden' name='add_remove_column_month' value='#{@startdate.strftime("%Y-%m")}' /> <input type='hidden' name='firsttime' value='#{@firsttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='lasttime' value='#{@lasttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='undo_revision' value='#{revision}' /> END # check, if some date of the row is unchecked if head_count("%Y-%m-%d",false).collect{|day,num| @data.include?(TimeString.new(day,time)) }.include?(false) # toggle all on ret += "<input type='hidden' name='toggleallon' value='true' />" ret += "<input type='hidden' name='new_columnname' value=\"#{CGI.escapeHTML(time)}\" />" titlestr = _("Select the whole row") else # toggle all off ret += "<input type='hidden' name='togglealloff' value='true' />" ret += "<input type='hidden' name='deletecolumn' value=\"#{CGI.escapeHTML(time)}\" />" titlestr = _("Deselect the whole row") end ret += "<input type='submit' class='toggle' title='#{titlestr}' value='#{MONTHFORWARD}' />" ret += <<END </div> </form> </td> END days.each{|day| timestamp = TimeString.new(day,time) klasse = "notchosen" klasse = "disabled" if timestamp < TimeString.now if @data.include?(timestamp) klasse = "chosen" hiddenvars = "<input type='hidden' name='deletecolumn' value=\"#{CGI.escapeHTML(timestamp.to_s)}\" />" else hiddenvars = "<input type='hidden' name='new_columnname' value=\"#{timestamp.date}\" />" if @data.include?(TimeString.new(day,nil)) # change day instead of removing it if no specific hour exists for this day hiddenvars += "<input type='hidden' name='columnid' value=\"#{TimeString.new(day,nil)}\" />" end end ret += "<td>" + add_remove_button(klasse, chosenstr[klasse], "columntime", CGI.escapeHTML(timestamp.time_to_s), revision, hiddenvars) + "</td>" } ret += "</tr>\n" } ret += timenavi(LATER,revision) ret += "<tr><td colspan='2' class='invisible'></td>" days.each{|d| ret += <<END <td class='addColumnManual'> <form method='post' action='' accept-charset='utf-8'> <div> <input type='hidden' name='new_columnname' value='#{d.strftime("%Y-%m-%d")}' /> <input type='hidden' name='add_remove_column_month' value='#{d.strftime("%Y-%m")}' /> <input type='hidden' name='firsttime' value='#{@firsttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='lasttime' value='#{@lasttime.to_s.rjust(2,"0")}:00' /> <input type='hidden' name='undo_revision' value='#{revision}' /> END if @data.include?(TimeString.new(d,nil)) ret += "<input type='hidden' name='columnid' value='#{TimeString.new(d,nil)}' />" end addstr = _("Add") hintstr = _("e. g., 09:30, morning, afternoon") ret += <<END <input type="text" name='columntime' title='#{hintstr}' style="max-width: 10ex" /><br /> <input type="submit" value="#{addstr}" style="width: 100%" /> </div> </form> </td> END } ret += "</tr></table>" ret += "<div class='error textcolumn'>#{@edit_column_error}</div>" if @edit_column_error end ret += "</td></tr></table>" ret end end