乐者为王

Do one thing, and do it well.

no such file to load -- xxx (MissingSourceFile) on Heroku

终于解决掉了困扰我多日的一个问题。

部署在Heroku上的项目,在控制器中require 'calc'时总是出现:

1
no such file to load -- calc (MissingSourceFile)

本地机器上运行的好好的,在Heroku上就出现Application error,百思不得其解,网上也没有找到类似问题的解决方法。

咨询Heroku的技术支持,几封邮件交流下来对方也没有办法(感觉Heroku的技术支持不咋的)。

今天把问题解决掉后再回过头来看,发现这其实是个非常简单的问题,就是Linux和Windows对待文件名大小写的差异。因为Linux是区分大小写的,所以用require 'calc'引入Calc.rb时就会失败,而Windows则可以成功。将文件名Calc.rb改成calc.rb再push到Heroku就把问题解决了。

就这么个大写字母竟然让我思考了几天,还真是郁闷啊!

实现带Icon的ListView

main.xml不需要修改,还是用原来的那个:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

在list_item.xml中添加ImageView控件,设置高度和宽度分别为32dip,这是因为有些应用Icon尺寸比较大的缘故,如果不设置的话有些行就会撑大:

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/app_icon"
        android:layout_gravity="center_vertical"
        android:layout_width="32.0dip"
        android:layout_height="32.0dip"
        android:layout_marginLeft="3.0dip"
        android:layout_marginRight="3.0dip" />
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16.0dip"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="13.0dip" />
    </LinearLayout>
</LinearLayout>

新建SimpleIconAdapter类,继承SimpleAdapter适配器:

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
import java.util.List;
import java.util.Map;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.SimpleAdapter;

public class SimpleIconAdapter extends SimpleAdapter {

    public SimpleIconAdapter(Context context, List<? extends Map<String, ?>> data,
            int resource, String[] from, int[] to) {
        super(context, data, resource, from, to);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View view = super.getView(position, convertView, parent);
        ImageView iv = (ImageView)view.findViewById(R.id.app_icon);
        iv.setImageDrawable((Drawable)((Map)getItem(position)).get("app_icon"));
        return view;
    }
}

修改MainActivity文件,更换绑定的适配器:

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
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {
    private static final String APP_ICON = "app_icon";
    private static final String APP_TITLE = "app_title";
    private static final String APP_PACKAGE = "app_package";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.list_view);
        SimpleAdapter adapter = new SimpleIconAdapter(this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {APP_ICON, APP_TITLE, APP_PACKAGE},
                new int[] {R.id.app_icon, R.id.app_title, R.id.app_package});
        listView.setAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            Drawable icon = pkg.applicationInfo.loadIcon(getPackageManager());
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put(APP_ICON, icon);
            map.put(APP_TITLE, label + " " + version);
            map.put(APP_PACKAGE, pkgName);
            listItem.add(map);
        }
        return listItem;
    }
}

不使用ListActivity实现ListView

main.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ListView
        android:id="@+id/list_view"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

list_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <LinearLayout
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1.0">
        <TextView
            android:id="@+id/app_title"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="16dip" />
        <TextView
            android:id="@+id/app_package"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>
</LinearLayout>

创建一个继承自Activitiy的类,覆写onCreate方法:

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
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.Activity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.SimpleAdapter;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        ListView listView = (ListView)findViewById(R.id.list_view);
        SimpleAdapter adapter = new SimpleAdapter(this,
                getInstalledApps(false),
                R.layout.list_item,
                new String[] {"app_title", "app_package"},
                new int[] {R.id.app_title, R.id.app_package});
        listView.setAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> listItem = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("app_title", label + " " + version);
            map.put("app_package", pkgName);
            listItem.add(map);
        }
        return listItem;
    }
}

至此,一个不使用ListActivity的ListView就完成了。

在ListView中显示已安装应用的名字和包名

通过PackageManager获取手机中已安装应用的信息,具体代码如下:

1
2
PackageManager packageManager = this.getPackageManager();
List<PackageInfo> packageInfoList = packageManager.getInstalledPackages(0);

通过以上方法,可以得到所有已安装的应用程序,既包括了手动安装的应用程序的信息,也包括了系统预装的应用程序的信息。要区分这两类应用可使用以下方法:

  1. 从packageInfoList获取的packageInfo,再通过packageInfo.applicationInfo获取applicationInfo;
  2. 判断applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM的值,该值大于0时,表示获取的应用为系统预装的应用,反之则为手动安装的应用。

列表的显示需要三个元素:

  1. 用来显示数据的ListView;
  2. 需要显示的数据;
  3. 绑定数据和ListView的Adapter。

Adapter通常有ArrayAdapter、SimpleAdapter和CursorAdapter等。其中SimpleAdapter有最好的扩充性,可以自定义出各种效果。

1
SimpleAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)

参数解释:

  1. context:上下文;
  2. data:数据列表。列表里的每一项都是一个Map对象,都和ListView里的一项进行数据绑定;
  3. resource:布局资源。可以引用系统提供的,也可以自定义;
  4. from:名字数组。每个名字都是用来索引数据列表中每个Map里的Object;
  5. to:资源索引数组,以R.id.xxx的形式表示。

main.xml布局代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:id="@+id/app_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="22px" />
    <TextView
        android:id="@+id/app_package"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>

上面代码中的android:orientation="vertical"很重要,没有它两个TextView就不会显示成两行。

继承了ListActivity的MainActivity代码:

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
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import android.app.ListActivity;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import android.widget.SimpleAdapter;

public class MainActivity extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        SimpleAdapter adapter = new SimpleAdapter(this,
                getInstalledApps(false),
                R.layout.main,
                new String[] {"app_title", "app_package"},
                new int[] {R.id.app_title, R.id.app_package});
        setListAdapter(adapter);
    }

    private List<HashMap<String, Object>> getInstalledApps(boolean getSysPackages) {
        List<HashMap<String, Object>> list = new ArrayList<HashMap<String, Object>>();

        List<PackageInfo> pkgs = getPackageManager().getInstalledPackages(0);
        for (int i = 0; i< pkgs.size(); i++) {
            PackageInfo pkg = pkgs.get(i);
            if (!getSysPackages && (pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) > 0) {
                continue;
            }
            String label = pkg.applicationInfo.loadLabel(getPackageManager()).toString();
            String version = pkg.versionName;
            String pkgName = pkg.packageName;

            HashMap<String, Object> map = new HashMap<String, Object>();
            map.put("app_title", label + " " + version);
            map.put("app_package", pkgName);
            list.add(map);
        }
        return list;
    }
}

构建支持OAuth的新浪微博桌面客户端

要使用OAuth,首先要去新浪微博注册一个自己的微博应用。注册之后,会得到微博应用的Consumer key和Consumer secret,都是一个字符串。之后就可以进行OAuth的认证过程了。

https://code.google.com/p/sina-weibo4j/ 下载新浪微博的Java SDK包,当然也可以在 http://open.weibo.com/wiki/index.php/SDK 下载。推荐使用前者,已经打好包了。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Properties;
import java.util.PropertyResourceBundle;

import weibo4j.Weibo;
import weibo4j.WeiboException;
import weibo4j.http.AccessToken;
import weibo4j.http.RequestToken;

public class UpdateStatus {
    private static final String ACCESS_SECRET = "access_secret";
    private static final String ACCESS_TOKEN = "access_token";
    private static final String FILE_NAME = "sina_token.properties";

    public static void main(String[] args) {
        // 在这里填写在应用申请中得到的App Key和App Secret
        System.setProperty("weibo4j.oauth.consumerKey", YOUR_CONSUMER_KEY);
        System.setProperty("weibo4j.oauth.consumerSecret", YOUR_CONSUMER_SECRET);

        Weibo weibo = new Weibo();

        try {
            // 读取存储起来的Access Token
            AccessToken accessToken = loadAccessToken();
            if (accessToken == null) {
                String backUrl = "http://zhubabo.appspot.com";

                // 请求Request Token(未授权令牌)
                RequestToken requestToken = weibo.getOAuthRequestToken(backUrl);
                System.out.println("Got request token...");
                System.out.println("Request token: " + requestToken.getToken());
                System.out.println("Request token secret: " + requestToken.getTokenSecret());

                // 将以下打印出的授权链接在浏览器中打开,完成应用授权,会自动跳转到
                // 指定的callback url,并将oauth_verifier一起返回
                System.out.println("Open the following url in a browser");
                System.out.println(" http://api.t.sina.com.cn/oauth/authorize?oauth_token=" + requestToken.getToken());

                // 输入上面得到的oauth_verifier的值,取得Access Token,这个Token是长期有效的
                System.out.println("please input verifier:");
                String verifier = readLine();
                accessToken = weibo.getOAuthAccessToken(
                    requestToken.getToken(),
                    requestToken.getTokenSecret(),
                    verifier);

                // 将Access Token保存下来,以后就可以直接通过此Token向新浪围脖发消息了
                storeAccessToken(accessToken.getToken(), accessToken.getTokenSecret());
            }

            weibo.setToken(accessToken.getToken(), accessToken.getTokenSecret());

            System.out.println("Input message to sina:");
            String message = readLine();
            while (!"exit".equals(message.trim())) {
                weibo.updateStatus(message);
                System.out.println("Input message to sina:");
                message = readLine();
            }
        } catch (WeiboException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void storeAccessToken(String token, String tokenSecret) throws IOException {
        OutputStream out = new FileOutputStream(new File(FILE_NAME));
        Properties p = new Properties();
        p.setProperty(ACCESS_TOKEN, token);
        p.setProperty(ACCESS_SECRET, tokenSecret);
        p.store(out, "sina access token");
        out.flush();
        out.close();
    }

    private static AccessToken loadAccessToken() throws IOException {
        File f = new File(FILE_NAME);
        if (!f.exists()) {
            return null;
        }
        InputStream in = new FileInputStream(f);
        PropertyResourceBundle bundle = new PropertyResourceBundle(in);
        in.close();
        return new AccessToken(bundle.getString(ACCESS_TOKEN), bundle.getString(ACCESS_SECRET));
    }

    private static String readLine() throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        byte[] bs = reader.readLine().getBytes("gbk");
        return new String(bs);
    }
}

Rails 2.3和Paperclip实现多图片上传(改进版)

从Rails 2.3开始,有了新的方法来处理多模型表单,这就是accepts_nested_attributes_for,它允许直接赋值到子对象上,对于标准的属性使用相同的散列格式。

例如,一个parent有多个children,包含所有类的POST数据将会是下面这样:

1
2
3
4
5
6
7
8
9
10
11
params =>
  action => update
  id => 1
  controller => parents
  parent =>
    first_name => John
    last_name => Doe
    age => 40
    children_attributes =>
      1 => { id => 16, :name => Jack }
      2 => { id => 18, :name => Mary }

注意,那个children_attributes元素在parent的元素中。fields_for会为这些子元素生成必要的POST数据,这些数据会被转换成方便accepts_nested_attributes_for解释的散列。children_attributes不是数组而是散列,它的键是一个简单的索引(不是模型的ID),被用来从一个单一的实体聚合属性。

不用ID的理由是简单的:正在被编辑的模型可能不会被保存,发送到了客户端,这时它的ID为nil。因此为每个被显示的children的id关联一个隐藏字段是个好的实践。

下面我们就用accepts_nested_attributes_for来改写上传多图片的代码。

修改albums_controller.rb中的new方法,删除下面的这句代码:

1
1.upto(3) { @album.photos.build }

将album.rb中的

1
2
3
4
5
def photo_attributes=(photo_attributes)
  photo_attributes.each do |attributes|
    photos.build(attributes)
  end
end

代码改成

1
accepts_nested_attributes_for :photos

修改_form.html.erb中的

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :collection => @album.photos %>
  <% end %>
</div>

<%= link_to_function "Add Photo" do |page| page.insert_html :bottom, :photos, :partial => 'photo', :object => Photo.new end %>

为下面的代码

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :locals => { :form => f, :photo => @album.photos.build } %>
  <% end %>
</div>

<%= add_object_link("Add Photo", f, @album.photos.build, "photo", "#photos") %>

再将_photo.html.erb中的

1
2
3
4
5
<% fields_for "album[photo_attributes][]", photo do |p| %>
  <%= p.label :photo %><br />
  <%= p.file_field :data, :index => nil %>
  <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
<% end %>

改为

1
2
3
4
5
<% form.fields_for :photos, photo, :child_index => (photo.new_record? ? "index_to_replace_with_js" : nil) do |p| %>
  <%= p.label :photo %><br />
  <%= p.file_field :data %>
  <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
<% end %>

在albums_helper中添加两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add_object_link(name, form, object, partial, where)
  html = render(:partial => partial, :locals => { :form => form }, :object => object)
  link_to_function name, %{
    var new_object_id = new Date().getTime();
    var html = jQuery(#{js html}.replace(/index_to_replace_with_js/g, new_object_id)).hide();
    html.appendTo(jQuery("#{where}")).slideDown('slow');
  }
end

def js(data)
  if data.respond_to? :to_json
    data.to_json
  else
    data.inspect.to_json
  end
end

到这里就修改完成了。试试看,是不是和原来的效果一样。

Rails 2.3和Paperclip实现多图片上传

1
2
3
4
5
6
7
8
9
rails upload
cd upload
script/plugin install git://github.com/thoughtbot/paperclip.git
script/plugin install git://github.com/aaronchi/jrails.git
script/generate scaffold album name:string
script/generate model photo album:references
script/generate paperclip photo data
rake db:migrate
script/server

修改album.rb添加photo_attributes属性:

1
2
3
4
5
6
7
8
9
10
class Album < ActiveRecord::Base
  has_many :photos
  validates_presence_of :name

  def photo_attributes=(photo_attributes)
    photo_attributes.each do |attributes|
      photos.build(attributes)
    end
  end
end

修改photo.rb添加附件相关属性:

1
2
3
4
5
6
7
8
9
10
class Photo < ActiveRecord::Base
  belongs_to :album

  has_attached_file :data,
    :url => "/uploads/:style_:basename.:extension",
    :styles => { :thumb => "50x50#", :large => "640x480#" }
  validates_attachment_presence :data
  validates_attachment_content_type :data,
    :content_type => ['image/jpeg', 'image/jpg', 'image/png']
end

修改new.html.erb为以下代码:

1
2
3
4
5
6
7
8
<h1>New album</h1>

<% form_for @album, :html => { :multipart => true } do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
  <p><%= f.submit "Create" %></p>
<% end %>

<%= link_to 'Back', albums_path %>

对edit.html.erb做同样的修改:

1
2
3
4
5
6
7
8
9
<h1>Editing album</h1>

<% form_for @album, :html => { :multipart => true } do |f| %>
  <%= render :partial => 'form', :locals => { :f => f } %>
  <p><%= f.submit "Update" %></p>
<% end %>

<%= link_to 'Show', @album %> |
<%= link_to 'Back', albums_path %>

把从new.html.erb和edit.html.erb中抽取出来的代码保存为_form.html.erb文件:

1
2
3
4
5
6
<%= f.error_messages %>

<p>
  <%= f.label :name %>
  <%= f.text_field :name %>
</p>

然后在后面添加如下代码:

1
2
3
4
5
6
7
<div id="photos">
  <% if @album.new_record? %>
    <%= render :partial => 'photo', :collection => @album.photos %>
  <% end %>
</div>

<%= link_to_function "Add Photo" do |page| page.insert_html :bottom, :photos, :partial => 'photo', :object => Photo.new end %>

创建_photo.html.erb文件,代码如下:

1
2
3
4
5
6
7
8
9
<div class="photo">
  <p>
  <% fields_for "album[photo_attributes][]", photo do |p| %>
    <%= p.label :photo %><br />
    <%= p.file_field :data, :index => nil %>
    <%= link_to_function "delete", "remove_field($(this), ('.photo'))" %>
  <% end %>
  </p>
</div>

再在application.js中添加下列代码,这样就可以删除file字段了。

1
2
3
function remove_field(element, item) {
  element.up(item).remove();
}

然后修改albums_controller.rb中的new方法:

1
2
3
4
5
6
7
8
def new
  @album = Album.new
  1.upto(3) { @album.photos.build }

  respond_to do |format|
    format.html # new.html.erb
  end
end

这样,上传多文件的功能基本就完成了。下面就来实现显示和修改的功能。

在show.html.erb的末尾添加下列代码,上传成功后用来显示图片:

1
2
3
4
#loop through the albums photos
<% for photo in @album.photos %>
  <%= image_tag photo.data.url(:thumb) %>
<% end %>

修改albums_controller.rb中的edit和update方法,用来删除图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def edit
  @album = Album.find(params[:id])
  if @album.photos.first.nil?
    1.upto(3) { @album.photos.build }
  end
end

def update
  params[:photo_ids] ||= []
  @album = Album.find(params[:id])
  unless params[:photo_ids].empty?
    Photo.destroy_pics(params[:id], params[:photo_ids])
  end

  respond_to do |format|
    if @album.update_attributes(params[:album])
      flash[:notice] = 'Album was successfully updated.'
      format.html { redirect_to(@album) }
    else
      format.html { render :action => "edit" }
    end
  end
en

在photo.rb中加上下面的代码:

1
2
3
def self.destroy_pics(album, photos)
  Photo.find(photos, :conditions => { :album_id => album }).each(:destroy)
end

然后新建_album_photo.html.erb,代码如下:

1
2
3
4
<% unless album_photo.new_record? %>
  <%= image_tag album_photo.data.url(:thumb) %>
  <%= check_box_tag "photo_ids[]", album_photo.id %>
<% end rescue nil %>

接着在_form.html.erb中加入下面的代码就可以删除图片了:

1
2
3
<div class="album_photos">
  <%= render :partial => 'album_photo', :collection => @album.photos %>
</div>

在Rails应用中集成支付宝

注意:notify_url为服务器通知,支付宝可以保证99.9999%的通知到达率,前提是你的网络通畅,在notify_url中可以做对数据库的业务操作。return_url中可以做数据库的更新也可以做显示。第一次交易状态改变(即时到帐中的交易完成的交易状态)时,支付宝发起的通知的时间与返回页自动跳转回的时间近乎同时。

支付处理代码:

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
class OrdersController < ApplicationController
  before_filter :login_required

  # place order
  def place_order
    parameters = {
      'service' => 'create_direct_pay_by_user',
      'partner' => ALIPAY_ACCOUNT,
      'seller_email' => ALIPAY_EMAIL,
      'out_trade_no' => @order.out_trade_no,
      'subject' => 'payment subject',
      'body' => 'payment body',
      'price' => @order.price.to_s,
      'quantity' => @order.quantity.to_s,
      'payment_type' => '1',
      '_input_charset' => 'utf-8',
      'notify_url' => url_for(:only_path => false, :action => 'notify'),
      'return_url' => url_for(:only_path => false, :action => 'done')
    }

    # 即时到帐中交易状态为“等待买家付款”的状态默认是不会发送通知的,自己手动设置一下
    @order.status = 'WAIT_BUYER_PAY'
    @order.user = current_user
    @order.save

    values = {}
    # 支付宝要求传递的参数必须要按照首字母的顺序传递,所以这里要sort
    parameters.keys.sort.each do |k|
      values[k] = parameters[k];
    end

    # 一定要先unescape后再生成sign,否则支付宝会报ILLEGAL SIGN
    sign = Digest::MD5.hexdigest(CGI.unescape(values.to_query) + ALIPAY_KEY)
    gateway = 'https://www.alipay.com/cooperate/gateway.do?'
    redirect_to gateway + values.to_query + '&sign=' + sign + '&sign_type=MD5'
  end

  # 返回success或fail。如果返回fail,支付宝会每隔一段时间就自动调用notify_url通信接口
  def notify
    render :text => 'success'
  end

  def done
    if verify_sign
      order = Order.find_by_out_trade_no(params[:out_trade_no])
      # 支付宝即时到帐接口只有一种交易状态,就是“交易成功”,更新一下
      order.update_attributes(params[:trade_status])
      render :text => 'Payment successful'
    else
      render :text => 'Alipay Error: ILLEGAL_SIGN'
    end
  end

protected
  def verify_sign
    params.delete("sign_type")
    sign = params.delete("sign")

    values = {}
    params.keys.sort.each do |k|
      values[k] = params[k];
    end

    sign.downcase == Digest::MD5.hexdigest(CGI.unescape(values.to_query) + ALIPAY_KEY)
  end
end

使用wizardly插件创建multi-step wizard

wizardly是一个非常容易使用的创建multi-step wizard的Rails插件,只需要三步就可以。

安装插件:

1
2
gem install wizardly
script/generate scaffold user first_name:string last_name:string age:integer gender:boolean

第一步:

1
2
3
class User < ActiveRecord::Base
  validation_group :step1, :fields => [:first_name, :last_name]
  validation_group :step2, :fields => [:age, :gender]

第二步:

1
2
class UsersController < ApplicationController
  act_wizardly_for :user

第三步:

1
script/generate wizardly_scaffold users

现在你就可以通过访问http://localhost:3000/users/step1来查看效果了。

随机化Paperclip的上传文件名

有时我们希望可以设定上传文件名的格式。一是可以统一文件名,而不是一些乱七八糟的名字;二是可以让文件名不是那么容易被猜测出来。下面的这段代码就是在网上找到的:

1
2
3
4
5
6
7
8
9
10
11
class Photo < ActiveRecord::Base
  has_attached_file :image, :url => "/uploads/:basename.:extension"

  before_create :randomize_file_name

  private
  def randomize_file_name
    extension = File.extname(image_file_name).downcase
    self.image.instance_write(:file_name, "#{Time.now.strftime("%Y%m%d%H%M%S")}#{rand(1000)}#{extension}")
  end
end

代码是从 http://trevorturk.com/2009/03/22/randomize-filename-in-paperclip/ 找到的,这里我把随机参数给改了,这样文件名的格式就类似20110310095632768这样。还有要注意的就是在:url中必须使用:basename参数,因为修改的:file_name就是它。