Archive for the 'plugins' Category

XML Sitemap Generator for Rails

I realy like the way the Google (XML) Sitemaps Generator for WordPress handles the generation of my sitemaps and informs Google about the changes on my blog.

I was missing this in the Rails world for a long time, so I decided to build my own Rails plugin.

Here it is: “XML Sitemap Generator for Rails”

It’s just a quick implementation of all the functionality you need to let your Rails App generate a XML Sitemap and ping Google about the updates.
It’s not made to be scale more to give your small site the abillitiy to have a sitemap.

I’ll work on it in the future to make it easy to add some custom URL’s and maybe to have a version to scale…

Check it out on: GitHub
If you have any suggestions fork the project and send me a pull request.

render_nested_form Helper

Die in Rails 2.3 eingeführten Nested Forms haben mir am Anfang sehr viele Kopfschmerzen bereitet und tun dies immer mal wieder.

Dank schorsch vom SalesKing muss ich mir um diese Dinge keine Sorgen mehr machen.
Sein render_nested_form Helper übernimmt alles und hat ein paar sehr nette Options.

# Options are:
# * <tt>:new</tt> - specify a certain number of new elements to be added to the form. Useful for displaying a few blank elements at the bottom.
# * <tt>:name</tt> - override the name of the association, both for the field names, and the name of the partial
# * <tt>:partial</tt> - specify the name of the partial in which the form is located.
# * <tt>:fields_for</tt> - specify additional options for the fields_for_associated call
# * <tt>:locals</tt> - specify additional variables to be passed along to the partial
# * <tt>:render</tt> - specify additional options to be passed along to the render :partial call
# * <tt>:skip</tt> - array of elements which will be skipped, usefull if you already rendered a partial in the same form with parts of the data. eg. obj.addresses, render the firt address on top of form, render all the other addresses at the bottom
 
f.render_nested_form(@project.tasks, :new => 3, :partial=>'some_partial', :locals=>..)

Update:
Ich hab den Helper geforked und die Option :as hinzugefügt. Damit kann man den Namen der lokalen Variable definieren, wenn man den Partial z.B. für unterschiedliche Objekte und Attribute nutzt (bei mir ist das ein Upload Form).
Hier findet ihr den Fork.

Funkenrailsdav: Webdav with Rails e.g. for ical

So you want a rails application to give you a webdav? Good, railsdav can do this for you. However, it might take you an hour or two as well to figure out how it works :)

This plugin is a copy of the original railsdav plugin with some modifications to make it run out-of-the-box. Just drop this plugin into your newly created rails application and it becomes a webdav-server. It comes with Authentication, so you can publish and synchronize your ical-files without fear :)

This was done using Rails 2.3.2.

Get it here: http://github.com/funkensturm/railsdav

Presenting: The Funkengallery Demo App

Still in beta mode, but we are proud to present a Rails 2.3 application, modularized in plugins, fully I18n (German, English, Swedish), and of course published at github.

This funkengallery demo application includes demonstration of all our plugins:

  • acts_as_category
  • acts_as_identifiable
  • funkengallery
  • funkenlogin
  • funkenlogin
  • irobot
  • manipulify

Again, this is beta still, which means that for example the admin area wants your models to be called exactly “Category” and “User”. So not 100% modularized yet, but we’re getting there and you can already use it as a perfect standalone application ;)

What’s so special about this gallery is the very dynamic user rights management and the simplicity. It is designed for a complex category tree with many thousands of pictures. However, you might expect flickr design and you get… well… funkengallery. It is different and simpler, but really neat if you want to share your pictures quick, private and with individual user rights.

Screenshots and Demo

Check out our Downloads site to see how you can easily test your local demo.

FINALLY! RailsICalendar ical ics publish with ruby on rails

Das hat echt was Nerven gekostet, aber ich bin mehr als zufrieden mit dem Resultat.

Ich darf vorstellen: Wenn man seinen Kalender in Mac OS X (z. B. per Webdav) auf seinen Server lädt (bzw. synchronisiert) und auf diesem Server auch Ruby on Rails läuft, dann kann man seinen Kalender jetzt auf seiner Webseite veröffentlichen.

In fact I just realize I should better speak english, because someone’s German might be somewhat rusty :)

So again: You have ics files on your server (e.g. via webdav) and Ruby on Rails is running? Great, let’s publish your calendar. The idea came from the great PHPicalendar script.

This is what it will somewhat look like:
bild-1.png

I am sorry to not have made a plugin out of this yet, but, hey, the basics are there, help yourself :) If you have any questions feel free to comment.

Requirements:

  • Vpim plugin with sudo gem install vpim

Features:

  • Read several ICS files from a directory on the server
  • Parse all the ical events in them
  • Cache the current calendar in yaml files
  • (The cache will be refreshed when a ICS file was updated meanwhile)
  • HTML will be presented for the calendar
  • Currently you can only choose a date and see the next X days

A word on recurrence of events

  • It does do most of the recurrence rules!
  • Specifically: All that VPIM supports
  • PLUS: EXDATE is also supported!

The Code

Initializer (config/initializers/any_filename.rb)

1
2
3
4
5
# Path to icalendar *.ics files on your server
PATH_ICS       = "#{RAILS_ROOT}/private/calendars/"
PATH_ICS_CACHE = "#{RAILS_ROOT}/tmp/calendars/"
FileUtils::mkdir_p(PATH_ICS)       unless File.exists?(PATH_ICS)
FileUtils::mkdir_p(PATH_ICS_CACHE) unless File.exists?(PATH_ICS_CACHE)

Controller (app/controllers/calendars_controller.rb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
require 'vpim'
class CalendarsController < ApplicationController
 
  def index
    # Load parameters if submitted
    session[:date_year]  = params[:options]["date(1i)"] if !params[:options].blank? && !params[:options]["date(1i)"].empty?
    session[:date_month] = params[:options]["date(2i)"] if !params[:options].blank? && !params[:options]["date(2i)"].empty?
    session[:date_day]   = params[:options]["date(3i)"] if !params[:options].blank? && !params[:options]["date(3i)"].empty?
 
    # Load standard if nothing submitted
    session[:date_year]  = Time.now.year  if session[:date_year].blank?
    session[:date_month] = Time.now.month if session[:date_month].blank?
    session[:date_day]   = Time.now.day   if session[:date_day].blank?
 
    # Set variables
    @scope     = 7
    @events   = []
    @today    = Time.gm(session[:date_year], session[:date_month], session[:date_day])
    cachefile = File.join(PATH_ICS_CACHE, "#{@today.to_s(:ical)}_#{@scope}.yml")
 
    # Kill cache if outdated
    if File.exists?(cachefile)
      killcache = false
      Dir.glob(File.join(PATH_ICS, '*.ics')).each do |file|
        killcache = true if File.mtime(file) > File.mtime(cachefile)
      end
      if killcache
        Dir.glob(File.join(PATH_ICS_CACHE, '*.*')).each do |file|
          File.delete(file)
        end
      end
    end
 
    # Load calendar from cache
    if File.exists?(cachefile)
      @events = YAML.load_file cachefile 
    else
      # No cache, parse each icalendar *.ics file in PATH_ICS and check for event occurences
      Dir.glob(File.join(PATH_ICS, '*.ics')).each do |file|
        category = File.basename(file, '.ics')
        Vpim::Icalendar.decode(File.open(file)).each do |calendar|
          calendar.components do |event|
            for day in 0..@scope
              if start = event.occurs_in?(@today+(60*60*24*day), @today+(60*60*24)+(60*60*24*day))
                myend = start + (event.dtend - event.dtstart)
                @events < < {
                  'category' => category,
                  'day'      => day,
                  'start'    => start,
                  'duration' => ((event.dtend - event.dtstart) / 60).round, # In minutes
                  'end'      => myend,
                  'allday'   => start.hour == 0 && start.min == 0 && start.sec == 0 && myend.hour == 0 && myend.min == 0 && myend.sec == 0,
                  'data'     => event
                }
              end
            end
          end
        end
      end
      @events = @events.uniq # Just in case :)
      # Save cache
      File.open(cachefile, 'w') { |f| YAML.dump(@events, f) }
    end
  end
 
end

Single View (view/calendars/index.html.erb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
< %
  width  = 110      # Width of one day
  height = 6        # height of 15 minutes in pixels
  buffer = 28       # free space for dates in day column (at top of each day)
  minh   = 20       # Minimum of event height
  cutmornings = 120 # I don't have events between 0:00 and 6:00, cut these pixels off
%>
 
<div id="action">
 
< % for day in 0...@scope + 1 do 
  today = (@today+(60*60*24*day))
  case today.wday  
    when 0  
      dayclass = 'class="sunday"'
    when 6  
      dayclass = 'class="saturday"'
    else  
      dayclass = 'class="otherday"'
  end
  %>
  <div id="ical_day" <%= dayclass %>
    style=" width:  < %= width %>px;
            height: < %= height*96 + buffer - cutmornings %>px;
            left:   < %= day*width + day*10 %>px;">
    < % if today.year == Time.now.year && today.month == Time.now.month && today.day == Time.now.day %>
      <b>< %= 'Today' %></b>
    < % else %>
      < %= _(today.strftime("%a")) +', '+ today.to_s(:date) %>
    < % end %>
  </div>
< % end %>
 
< % tops = Array.new(@scope + 1, '')
@events.each do |event|
 
  # Exclude recurrence rule hack
  today = (@today+(60*60*24*event['day']))
  exme = false
  event['data'].propvaluearray('EXDATE').each do |exdate|
    exdate = exdate.to_time
    exme = true if today.year == exdate.year && today.mon == exdate.mon && today.day == exdate.day
  end
  next if exme # Skip this event
 
  if event['allday']
    # All-day events will be inserted later
    tops[event['day']] += '&nbsp; ' + event['data'].summary.to_s + '<br/>'
  else 
    eventheight = ((event['duration']/15)*height).round
    eventheight = minh if eventheight < minh
    %>
    <div id="ical_event"
      style=" background: #<%= eventcolor(event['category']) %>;
              width:  < %= width-2 %>px;
              height: < %= eventheight.to_s %>px;
              top:    < %= event['start'].hour*height*4 + (event['start'].min/15)*6 + buffer - cutmornings %>px;
              left:   < %= event['day']*width + event['day']*10 %>px;">
      < %= '<b>'+ event['start'].to_s(:time) +' - '+ event['end'].to_s(:time) +'<br />' %>
      < %= event['data'].summary %>
    </div>
  < % end %>
< % end %>
 
< % tops.each_with_index do |top, day| %>
    <div id="ical_event_top"
      style=" width: <%= width-2 %>px;
              height: < %= height %>px;
              top: < %= buffer %>px;
              left: < %= day * width + day * 10 %>px;">
      < %= top %>
    </div>
< % end %>
 
<div id="vertical_spacer" style="height: <%= height*96 + buffer*2 - cutmornings %>px;">&nbsp;</div>
 
</div>

Layout View (views/layouts/calendars.html.erb)

        < % form_tag :controller => 'calendars', :action => nil, :id => nil do |f| %>
          < %= date_select("options", "date", :default => @today, :order => [:day, :month, :year]) %>
            < %= submit_tag 'Show' + ' &raquo;', :class => 'date_button' %>
        < % end %>

Helper (for coloring events) (app/helpers/calendar_helper.rb)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module CalendarsHelper
 
  def eventcolor(category)
    case category  
      when 'Wichtig'   # This is the name of the .ics file
        return 'f66'
      when 'Sonstiges'
        return '4f4'
      when 'Studium'
        return 'fb4'
      when 'Privat'
        return '77f'
      when 'Freunde'
        return 'f4f'
      else  
        return 'fb4'
    end
  end
 
end

Stylesheet (public/stylesheets/calendar.css)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**************** DIVs ***************************/
 
div#action {
  position: absolute;
  margin-left: 10px;
  margin-top: 10px;
  padding: 4px;
  border-left: 0px;
}
 
div#ical_day {
  position: absolute;
  padding-top: 5px;
  text-align: center;
  font-size: 10px;
}
 
.otherday {
  background: #ddd;
}
 
.sunday {
  background: #fdd;
}
 
.saturday {
  background: #ddf;
}
 
#ical_event {
  position: absolute;
  border: 1px #555 solid;
  background: #fb4;
  font-size: 8px;
  text-align: left;
}
 
#ical_event_top {
  position: absolute;
  border: 0px;
  font-size: 9px;
  font-weight: bold;
  text-align: left;
}
 
/**************** Fonts ***************************/
 
#title {
  float:right;
  color: #ddd;
  margin-top: 30px;
  margin-right: 10px;
  line-height: 11px;
  font-size: 16px;
  text-align: right;
}
#title .description {
  color: #777;
  font-size: 10px;
  margin: 0;
}

Routes (could be optional) (config/routes.rb)

1
2
  # CALENDAR Controllers
  map.connect 'calendar', :controller => 'calendars'

Localization Plugin: Improved generate_l10n_file

Das Localization Plugin hat eine Methode generate_l10n_file um die zu lokalisierenden Strings auszulesen. Leider macht sie dies nicht besonders gut.

Auf die Schnelle habe ich sie etwas abgeändert und siehe da:

def self.generate_l10n_file
  "Localization.define('de') do |l|" < <
  Dir.glob("#{RAILS_ROOT}/app/**/*.*").collect do |f| 
    ["# #{f}"] << File.read(f).scan(/(<%=_ |_\()[\"\'](.*?)[\"\']/)
  end.uniq.flatten.collect do |g|
    g.starts_with?('#') ? "" : "  l.store '#{g}', ''"
  end.uniq.sort.join("\n").gsub("  l.store '_(', ''",'').gsub("  l.store '<%=_ ', ''",'') << "\nend"
end

Jetzt werden sämtliche Controller ausgelesen und es werden deutlich mehr Strings gefunden (nämlich auch die wo nicht %= davor steht).

UPDATE: Zu “#{RAILS_ROOT}/app/**/*.*” müsste man noch “#{RAILS_ROOT}/app/views/**/*.*” irgendwie hinzunehmen.

Hier noch ein rake task dafür:

namespace [:l10n] do
    desc 'Verbose localization file (experimental)'
    task :generate => :environment do
      puts
      puts 'Creating localization file...'
      puts '----------- SNIP -----------'
      puts Localization.generate_l10n_file
      puts '----------- SNAP -----------'
      puts
      puts 'Finished.'
      puts
    end
end

Plugin: acts_as_list_in_tree und descendants in acts_as_tree

Ich habe eine Kategorienliste mit Unterkategorien. Nun möchte ich acts_as_list verwenden, um die Reihenfolge mit den Feld “position” leichter ändern zu können. Das geht auch soweit (Dann hat man so tolle Funktionen wie Category.move_higher).

Leider gibt es da aber immer wieder Probleme, da eine neu hinzugefügte Kategorie die Position max(alle_positionen)+1 erhält. Also die höchste ALLER Positionen +1. Ich möchte aber, dass eine neu hinzugefügte Kategorie die position+1 bezogen auf seine Vaterkategorie erhält!

Also sich als Liste verhält, aber nur in Bezug auf seine Geschwister!

Dafür muss man das Plugin (Rails 2.0) “acts_as_list” ändern. Und zwar muss man die Zeile 64, auskommentieren:

##{scope_condition_method}

Und die Funktion scope_condition() in Zeile 186 wie folgt ändern:

# Overwrite this method to define the scope of the list changes
#def scope_condition() "1" end
def scope_condition() "parent_id = #{self.parent_id}" end

So habe ich mir mein eigenes acts_as_list_in_tree (download) Plugin gebaut.

Vielleicht noch als Anmerkung:
In acts_as_tree gibt es KEINE DESCENDANTS Funktion! Das heißt, man kann sich in einem Tree nicht die Nachkommen ausgeben lassen. Die Vorfahren schon, mit .ancestors, aber halt nicht die Nachkommen.

Siehe da HIER habe ich die Lösung gefunden. Ich hatte nur gehofft, sie sei in Rails 2.0 schon implementiert, ist sie aber nicht!

Also selber hinzufügen (am besten unter die ancestors funktion) in
/vendor/plugins/acts_as_tree/lib/active_record/acts/tree.rb:

# Bugfix - Returns list of descendants
def descendants 
  descendants = [] 
  self.children.each { |child| descendants += [child] + child.descendants } if self.children.length > 0 
  descendants
end

Wenn es in Rails 2.x übernommen wird, kann man es wieder löschen ;)

Das war’s für heute :)

Ruby on Rails: String manipulation

Es gibt einige Sachen, die ich gerne mit Strings machen möchte. Aber natürlich ist nicht ALLES in Rails schon drin. Also habe ich mir mit einem Plugin wie folgt geholfen.

z. B.: “2″.numstring? gibt mir an, ob der string nur Ziffern enthält.

/vendor/plugins/future/init.rb

require 'string_manipulation'

/vendor/plugins/future/lib/string_manipulation.rb

require 'digest/sha1'
module StringManipulation
 
  # Remove ALL unneccessary whitespaces from string
  #    "   hello   world   " #=> "hello world"
  def strip_all
    self.gsub(/ +/, ' ').strip.chomp
  end
 
  # Remove everything that is not a-z or 0-9 or space
  def strip_illegal
    self.gsub(/[^a-zA-Z0-9 ]/, '')
  end
 
  # Returns the basename of a file
  def strip_extention
    self.gsub(/(\.(.*))$/,'')
  end
 
  # Strips RAILS_ROOT/public from string
  def url_from_path
    self.gsub("#{RAILS_ROOT}/public", '')
  end
 
  # Strips RAILS_ROOT from string
  def root_from_path
    self.gsub("#{RAILS_ROOT}", '')
  end
 
  # Hashes a string with SHA1
  def hashed
    Digest::SHA1.hexdigest(self)
  end
 
  # Make a nice downcase keyword list as string, seperated by single spaces
  #    keyword_list("   My §$% KEY   wordlist   ") #=> "my key wordlist"
  def keywordlist
    self.strip_illegal.downcase.strip_all
  end
 
  # Checks if a string contains only numerical characters
  def numstring?
    self =~ /^\d+(\.\d+|\d*)$/
  end
 
end
 
class String
  include StringManipulation
end

Als Unterfunktion von Object sind die Funktionen überall verfügbar. Sehr praktisch.