Tag Archive for 'ruby on rails'

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.

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

Passenger für Ruby on Rails aus TextMate “automatisch” neu starten

Manchmal möchte man seine Ruby-on-Rails-Applikation im Passenger manuell neu starten. Sprich eine Datei my_app/tmp/restart.txt anlegen. Ich habe ein kleines Command-Skript für TextMate dafür geschrieben. Wenn man es ausführt (in diesem Beispiel mit APFEL+R), wird beim nächsten Browseraufruf alles neu geladen. Praktisch wenn man Plugins entwickelt :)

Hier der Command in TextMate:

restart_passenger

Und hier der Code dafür

18
19
20
21
22
23
24
#!/usr/bin/env ruby
 
require "#{ENV['TM_BUNDLE_SUPPORT']}/lib/rails_bundle_tools.rb"
f = File.open File.join(RailsPath.new.rails_root, "tmp", "restart.txt"), "w"
f.close
 
puts "Rails Application will be reloaded!"

Rails 2.2: NoMethodError von create_time_zone_conversion_attribute?

Nachdem ich jetzt mehrere Stunden nicht verstanden habe, wieso meine Rails Applikation nur einen Klick lang funktioniert, möchte ich euch dran teil haben lassen. Ich habe mehrere Libraries und Plugins die alle meine Models erweitern und dachte schon es läge daran, dass immer dieser Fehler auftauchte:


You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.include?

Recherche brachte mich hier her:
http://rails.lighthouseapp.com/projects/8994/tickets/1339

Das ganze lag nur daran, dass in Rails 2.2 ein Bug ist, der einen ein ActiveRecord Model nicht in eine Konstakte packen lässt!

Ich hatte eine Config-Datei die

1
MEINMODEL = MeinModelName

machte und später im Controller dann

1
MEINMODEL.find(....

Und das darf man nicht :) Jedenfalls noch nicht.

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'

Sessions aufräumen – Stale Sessions Clean Up

Wenn man den ganz normalen Session Handler von Rails benutzt, dann werden die Sessions unter tmp/sessions/ abgelegt. Rails räumt dieses Session Verzeichnis aber nicht selber auf. Das kann zu einem Problem werden, wenn man plötzlich tausende von Sessionfiles in diesem Verzeichnis findet. Erst einmal wird die Application dadurch langsamer und zum Zweiten kann es auch das File System des Servers belasten, wenn der nämlich plötzlich keine Nodes mehr machen kann.

Eine einfach Lösung für dieses Problem ist ein Cron Job der das Verzeichnis regelmäßig aufräumt und alle alten (stale) Sessions löscht.

Hierfür habe ich ein Shell Script geschrieben, was ich in den Script Ordner meines Rails Projektes abgelegt habe. So sieht es aus:
script/remove_stale_sessions.sh

#!/bin/sh
find ../tmp/sessions/ruby_sess.* -mtime +1 -print | xargs rm -rf

Das Script löscht alle Sessions die seit 1 Tag nicht mehr benutzt wurden. Jetzt fehlt noch der Cron Job, der das Script alle paar Minuten aufruft. Ich hab ihn mal auf alle 10 Minuten gestellt. Mit crontab -e ruft man den Cron Job Manager auf. Jetzt i drücken, damit man in den INSERT Mode kommt und in die letzte Zeile folgenden Code schreiben:

*/10 * * * * sh /pfad/zur/app/script/remove_stale_sessions.sh

esc drücken um den INSERT Mode zu verlassen und :wq zum speichern und schon sollte der neue Cron installiert sein.

Update:
Ach so… vielleicht sollte man dafür gleich nen Deployment Task oder so was schreiben… irgendwas, damit man halt nicht bei jedem Projekt dran denken muss… wenn jemand Vorschläge hat…

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

subversion auf debian aufgesetzt

Hi, endlich geschafft. SVN läuft und es war (im Rückblick gesehen) gar nicht so schwer. Dieser Artikel hat mir gut weitergeholfen. Nach der Installation von subversion mit aptitude hat folgende Einrichtung auf einem unserer debian server funktioniert:

Verzeichnis für Repositories auf dem Server erstellen:

mkdir /var/svn

Jetzt wird aus diesem Verzeichnis ein subversion repository gemacht:

svnadmin create /var/svn

Da der Server bisher nur über einen root Zugang verfügte, erstmal Benutzer anlegen, die subversion später nutzen werden:

useradd future
useradd manuel

Diese bekommen dann noch schöne Passwörter verpasst. Bei Eingabe wird ein gewünschtes Passwort für den jeweiligen user abgefragt.

passwd future
passwd manuel

Jetzt noch die home-verzeichnisse anlegen:

mkdir /home/future
mkdir /home/manuel
chown future:users /home/future
chown manuel:users /home/manuel

Damit wir uns später Arbeit sparen, legen wir eine Gruppe an, und setzen die neuen User in diese Gruppe. So ist es später leichter, Rechte für die Nutzung von SVN zu vergeben.

adduser future subversion
adduser manuel subversion

Jetzt bekommt die Gruppe (und somit future und manuel) Zugriff auf das vorhin erstellte svn repository. Der -R Parameter steht für rekursive, also alle Unterverzeichnisse inbegriffen (auf o. a. Artikel gibt es etwas mehr Details hierzu).

chgrp -R subversion /var/svn
chmod -R o-rwx /var/svn
chmod -R g+rw /var/svn
chmod g+s /var/svn/db

Jetzt wird es nochmal etwas tricky. Wenn oben genannte User Dateien erstellen und so weiter, dann werden diesen Dateien bestimmte Rechte gegeben. Nun wollen wir aber sicher stellen, dass niemand aus Versehen nur Rechte für sich selbst einräumt.

Dafür verwenden wir den UNIX Befehl umask. Wenn wir “umask 002″ aufrufen, heißt dass, dass von jetzt an bei Dateierstellung volle Rechte für die eigene Gruppe und lese und execute Rechte für alle andere gesetzt werden soll.

Da nicht jeder ständig diesen Befehl eingeben wird und möchte, “automatisieren” wir das ganz schlau. Nutzt jemand unser SVN repository, wird die Datei “svnserve” aufgerufen. Wir wechseln in das Verzeichnis, wo diese Datei sich befindet (“which svnserve” verrät uns wo das ist) und bennennen die Datei um! und zwar nehmen wir das letzte “e” weg:

cd /usr/bin
mv svnserve svnserv

Anschließend legen wir eine bash-Datei an, die genau so heißt wie unser svnserve vorher. In diese Datei schreiben wir folgendes:
Inhalt von /usr/bin/svnserve

#!/bin/bash
umask 002
/usr/bin/svnserv $*

Das bewirkt, dass jedes Mal wenn jemand svnserve (mit oder ohne Parameter) startet, er unser kleines Progrämmchen startet, dadurch automatisch “umask 002″ ausführt und dann erst das original svnserv. Schlau oder? (Den Trick habe ich übrigens hier gefunden.)

Jetzt befassen wir uns mit der Datei /var/svn/conf/svnserve.conf. Es handelt sich um die Konfiguration unseres svn repositories. In ihr passen wir ein paar Einstellungen wie folgt an:

[general]
anon-access = none
auth-access = write
realm = funkensturm

Damit geben wir anonymen usern “none” Rechte, future und manuel “write” rechte und noch einen Namen für unser Repository.

Auf dieser Seite habe ich ein paar Einstellungen gefunden, wie man den SSH Login usw. sicherer macht. Außerdem müssen wir ja noch unserer Gruppe subversion den Login per SSH erlauben!
Also ran an die Datei /etc/ssh/sshd_config (vorher backup machen!).
Die Datei ist ziemlich lang. Worauf es ankommt sind letzten Endes diese Werte:

Port 4444
AllowGroups subversion
LoginGraceTime 30
PermitRootLogin no

Damit haben wir den SSH Port von 22 (Standard) auf (z. B.) 4444 erhöht, damit nicht jeder Depp direkt den Port scannen kann. AllowGroups ist neu hinzugekommen, damit alle user in der Gruppe “subversion” SSH benutzen können. Die LoginGraceTime wurde auf 30 Sekunden gesetzt (Zeit zum Passwort eingeben). Da manuel und future sich jetzt einloggen können, können wir den login als root via SSH verbieten mit PermitRootLogin no.

Jetzt noch schnell SSH neu starten und die Änderungen sind aktiv:

/etc/init.d/ssh restart

FERTIG!

Ab jetzt kann man sich per SSH so auf dem Server (natürlich mit der richtigen IP und nicht 123…) einloggen:

ssh future@123.123.123.123 -p 4444

ABER!

Leider kapiert dein lokales svn nicht, dass er bei svn+ssh nicht den Port 22 nehmen soll, sondern unseren 4444. Dafür müssen wir LOKAL (also auf DEINEM MacBook oder PowerBook ;) folgende Datei anpassen: ~/.subversion/config

[tunnels]
fs = /usr/bin/ssh -p 4444

Damit sagen wir, dass ab jetzt svn+fs:// mit Port 4444 aufgerufen wird. (Man könnte auch anstelle “fs” auch “ssh” in die config Datei schreiben, dann würde bei svn+ssh:// immer Port 5777 verwendet werden. Aber das will man mit an Sicherheit grenzender Wahrscheinlichkeit nicht.)

Wenn man sein Projekt auschecken möchte, wird für unseren Server also ab jetzt das hier benutzt:

svn co svn+fs://future@123.123.123.123/var/svn/projekt

Zum hochladen eines neuen Projektes:

svn import projektname svn+fs://future@123.123.123.123/var/svn/projektname -m "Beschreibung der Änderung"

YEAH BABY!

Railscasts.com – die beste Ruby on Rails Resource

Auch wenn es die meisten wohl schon kennen, die Screencasts von Ryan Bates, sind meiner Meinung nach, so ziemlich die beste Resource, was praktische Tipps mit Ruby on Rails angeht. Er spricht so ziemlich alle wichtigen Themen an und bietet auch für etwas erfahrenere Entwickler manch eine Idee.
Also unbedingt anschauen, wer es noch nicht kennt:
www.railscasts.com

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.