marley 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ module Marley
2
+ class ModelController
3
+ def initialize(model)
4
+ @model=model
5
+ if $request[:path][1].to_s.match(/^\d+$/) #references a specific instance by ID
6
+ @instance=@model[$request[:path][1].to_i]
7
+ @method_name=$request[:path][2]
8
+ if @method_name
9
+ raise RoutingError unless @instance.respond_to?(@method_name)
10
+ @method=@instance.method(@method_name)
11
+ end
12
+ else #class method -- should yield 0 or more instances of model in an array
13
+ @method_name=$request[:path][1]
14
+ @method_name='list' if @method_name.nil? && $request[:verb]=='rest_get'
15
+ if @method_name
16
+ raise RoutingError unless @model.respond_to?(@method_name)
17
+ @method=@model.method(@method_name)
18
+ end
19
+ end
20
+ if (a=@instance || @model).requires_user?
21
+ raise AuthorizationError unless a.authorize(@method_name)
22
+ end
23
+ if @method && $request[:verb] != 'rest_post'
24
+ @instances=if p=$request[:get_params][@model.resource_name.to_sym]
25
+ @method.call(p)
26
+ elsif i=$request[:path][3]
27
+ @method.call[i.to_i]
28
+ else
29
+ @method.call
30
+ end
31
+ end
32
+ end
33
+ def rest_get; @instances || @instance; end
34
+ def rest_post
35
+ if @instance
36
+ raise RoutingError unless @method
37
+ params=$request[:post_params][@model.resource_name.to_sym][@method_name.to_sym] || $request[:post_params][@method_name.to_sym]
38
+ raise ValidationFailed unless params
39
+ params=[params] unless params.class==Array
40
+ params.map do |param|
41
+ @instance.send("add_#{@method_name}",param)
42
+ end
43
+ else
44
+ @instance=@model.new($request[:post_params][@model.resource_name.to_sym] || {})
45
+ @instance.save(@instance.write_cols)
46
+ @instance.respond_to?('create_msg') ? @instance.create_msg : @instance
47
+ end
48
+ end
49
+ def rest_put
50
+ raise RoutingError unless @instance
51
+ (@instances || [@instance]).map do |i|
52
+ i.modified!
53
+ i.update_only($request[:post_params][@model.resource_name.to_sym],i.write_cols)
54
+ end
55
+ end
56
+ def rest_delete
57
+ raise RoutingError unless @instance
58
+ if @instances
59
+ @instances.each do |instance|
60
+ meth="remove_#{instance.class}"
61
+ raise RoutingError unless @instance.respond_to?(meth)
62
+ @instance.send(meth,instance)
63
+ end
64
+ else
65
+ @instance.destroy
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,27 @@
1
+ module Marley
2
+ module Joints
3
+ MR=Marley::Resources
4
+ MJ=Marley::Joints
5
+ class Joint
6
+ def initialize(opts={})
7
+ config(opts)
8
+ end
9
+ def smoke
10
+ klass=self.class
11
+ { 'resources' => lambda {|c| MR.const_set(c,klass::Resources.const_get(c))},
12
+ 'class_methods' => lambda {|c| MR.const_get(c).extend klass::ClassMethods.const_get(c)},
13
+ 'instance_methods' => lambda {|c| klass::InstanceMethods.const_get(c).send :append_features, MR.const_get(c)}
14
+ }.each_pair do |mod_name, importer|
15
+ if klass.constants.include?(mod_name.camelize)
16
+ klass.const_get(mod_name.camelize).constants.each do |c|
17
+ importer.call(c) unless @opts[mod_name.to_sym] && ! @opts[mod_name.to_sym].include?(c.underscore)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ def config(opts)
23
+ @opts=(@opts || {}).merge(opts)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+
2
+ module Sequel::Plugins::RestSection
3
+ SECTION_PROPS='name','title','description','navigation'
4
+ module ClassMethods
5
+ SECTION_PROPS.each {|p| attr_accessor :"section_#{p}"}
6
+ def section
7
+ if SECTION_PROPS.find {|p| send(:"section_#{p}").to_s > ''}
8
+ Marley::ReggaeSection.new [SECTION_PROPS.inject({}) {|props,p| props[p.to_sym]=send(:"section_#{p}");props }]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ Sequel::Model.plugin :rest_section
14
+ module Marley
15
+ module Joints
16
+ class BasicMenuSystem < Joint
17
+ module Resources
18
+ class Menu
19
+ class <<self
20
+ attr_accessor :sections
21
+ end
22
+ attr_accessor :title,:name,:description, :navigation
23
+ def self.rest_get
24
+ new.to_json
25
+ end
26
+ def self.requires_user?
27
+ ! $request[:path].to_a.empty?
28
+ end
29
+ def initialize
30
+ @name='main'
31
+ if $request[:user].new?
32
+ u=$request[:user].to_a
33
+ u[1].merge!({:description => 'If you don\'t already have an account, please create one here:'})
34
+ @title="Welcome to #{$request[:opts][:app_name]}"
35
+ @description='Login or signup here.'
36
+ @navigation=[LOGIN_FORM,u]
37
+ else
38
+ @title = "#{$request[:opts][:app_name]} Main Menu"
39
+ @description="Welcome to #{$request[:opts][:app_name]}, #{$request[:user].name}"
40
+ @navigation=(self.class.sections || MR.constants).map do |rn|
41
+ if (resource=MR.const_get(rn)).respond_to?(:section) && (s=resource.section)
42
+ [:link,{:title => s.title, :description =>s.description, :url => "#{resource.resource_name}/section" }]
43
+ end
44
+ end.compact
45
+ end
46
+ end
47
+ def to_json
48
+ [:section,{:title => @title,:description => @description,:name => @name, :navigation => @navigation}]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,88 @@
1
+ require 'sanitize'
2
+
3
+ module Marley
4
+ module Joints
5
+ class BasicMessaging < Joint
6
+ module Resources
7
+ class Message < Sequel::Model
8
+ plugin :single_table_inheritance, :message_type, :model_map => lambda{|v| v ? MR.const_get(v.to_s) : ''}, :key_map => lambda{|klass|klass.name.sub(/.*::/,'')}
9
+ plugin :tree
10
+ many_to_one :author, :class => :'Marley::Resources::User'
11
+ @owner_col=:author_id
12
+ def rest_cols; [:id,:author_id,:message,:title,:parent_id]; end
13
+ def write_cols; new? ? rest_cols - [:id] : []; end
14
+ def required_cols; write_cols - [:parent_id]; end
15
+ def rest_schema
16
+ super << [:text,:author,RESTRICT_RO,author.to_s]
17
+ end
18
+ def authorize_rest_get(meth)
19
+ current_user_role && (meth.nil? || get_actions.include?(meth))
20
+ end
21
+ def authorize_rest_put(meth); false; end
22
+ def after_initialize
23
+ super
24
+ if new?
25
+ self.author_id=$request[:user][:id]
26
+ self.thread_id=parent ? parent.thread_id : Message.select(:max.sql_function(:thread_id).as(:tid)).all[0][:tid].to_i + 1
27
+ end
28
+ end
29
+ def before_save
30
+ self.message=Sanitize.clean(self.message,:elements => %w[blockquote em strong ul ol li p code])
31
+ end
32
+ def validate
33
+ validates_presence [:author,:message,:title]
34
+ validates_type MR::User, :author
35
+ end
36
+ def thread
37
+ children.length > 0 ? to_a << children.map{|m| m.thread} : to_a
38
+ end
39
+ end
40
+ class PrivateMessage < Message
41
+ def get_actions;['reply','reply_all'];end
42
+ def rest_cols; super << :recipients; end
43
+ def current_user_role
44
+ super || (recipients.match(/\b#{$request[:user][:name]}\b/) && "recipient")
45
+ end
46
+ def authorize_rest_get(meth)
47
+ super && ($request[:user]==author || self.recipients.match(/\b#{$request[:user].name}\b/))
48
+ end
49
+ def authorize_rest_post(meth)
50
+ meth.to_s > '' && (author_id==$request[:user][:id] || recipients.match(/\b#{$request[:user][:name]}\b/))
51
+ end
52
+ def self.authorize_rest_post(asdf)
53
+ true #may need to change this, for now auth is handled in validation
54
+ end
55
+ def reply
56
+ self.class.new({:parent_id => self[:id],:author_id => $request[:user][:id],:recipients => author.name, :title => "re: #{title}"})
57
+ end
58
+ def reply_all
59
+ foo=reply
60
+ foo.recipients="#{author.name},#{recipients}".gsub(/\b(#{$request[:user][:name]})\b/,'').sub(',,',',')
61
+ foo
62
+ end
63
+ def validate
64
+ super
65
+ validates_presence [:recipients]
66
+ self.recipients.split(',').each do |recipient|
67
+ if u=MR::User[:name => recipient]
68
+ errors.add(:recipients, "You may only send PM's to Admins or Mods. #{recipient} is neither of those") unless (['Admin','Moderator'].include?(MR::User[:name => recipient].user_type) || [MR::Admin,MR::Moderator].include?($request[:user].class))
69
+ else
70
+ errors.add(:recipients, "Invalid user: #{recipient}")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ class Post < Message
76
+ def get_actions;['reply'];end
77
+ def current_user_role
78
+ super || 'reader'
79
+ end
80
+ def authorize_rest_post(meth);true;end
81
+ def reply
82
+ self.class.new({:parent_id => self[:id],:author_id => $request[:user][:id], :title => "re: #{title}"})
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,51 @@
1
+ require 'digest/sha1'
2
+ LOGIN_FORM= [:instance,{:url => 'login',:description => 'Existing users please log in here:',:new_rec => true,:schema => [[:text,'name',RESTRICT_REQ],[:password,'password',RESTRICT_REQ]]}]
3
+ module Marley
4
+ module Joints
5
+ class BasicUser < Joint
6
+ module Resources
7
+ class User < Sequel::Model
8
+ set_dataset :users
9
+ plugin :single_table_inheritance, :user_type, :model_map => lambda{|v| MR.const_get(v.to_sym)}, :key_map => lambda{|klass|klass.name.sub(/.*::/,'')}
10
+ attr_reader :menus
11
+ attr_accessor :old_password,:password, :confirm_password
12
+ def self.requires_user?
13
+ ! ($request[:verb]=='rest_post')
14
+ end
15
+ def self.section
16
+ ReggaeSection.new([ {:title => 'User Info', :name => self.to_s.underscore, :navigation => []},$request[:user]]) if $request[:user].class == self
17
+ end
18
+ def write_cols;[:name,:email,:password,:confirm_password,:old_password];end
19
+ def rest_schema
20
+ schema=super.delete_if {|c| [:pw_hash,:description,:active].include?(c[NAME_INDEX])}
21
+ schema.push([:password,:old_password,0]) unless new?
22
+ schema.push([:password,:password ,new? ? RESTRICT_REQ : 0],[:password,:confirm_password,new? ? RESTRICT_REQ : 0])
23
+ schema
24
+ end
25
+ def self.authenticate(credentials)
26
+ u=find(:name => credentials[0], :pw_hash => Digest::SHA1.hexdigest(credentials[1]))
27
+ u.respond_to?(:user_type) ? Marley::Resources.const_get(u[:user_type].to_sym)[u[:id]] : u
28
+ end
29
+ def validate
30
+ super
31
+ validates_presence [:name]
32
+ validates_unique [:name]
33
+ if self.new? || self.old_password.to_s + self.password.to_s + self.confirm_password.to_s > ''
34
+ errors[:password]=['Password must contain at least 8 characters'] if self.password.to_s.length < 8
35
+ errors[:confirm_password]=['Passwords do not match'] unless self.password==self.confirm_password
36
+ errors[:old_password]=['Old Password Incorrect'] if !self.new? && Digest::SHA1.hexdigest(self.old_password.to_s) != self.pw_hash
37
+ end
38
+ end
39
+ def before_save
40
+ if self.new? || self.old_password.to_s + self.password.to_s + self.confirm_password.to_s > ''
41
+ self.pw_hash=Digest::SHA1.hexdigest(self.password)
42
+ end
43
+ end
44
+ def create_msg
45
+ [[:msg,{:title => 'Success!'},"Your login, '#{self.name}', has been sucessfully created. You can now log in."]]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,122 @@
1
+ Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS.merge!(:presence => {:message => 'is required'})
2
+ Sequel::Model.plugin :timestamps, :create => :date_created, :update => :date_updated
3
+
4
+ Marley.joint 'basic_user',{:resources => []}
5
+ Marley.joint 'tagging'
6
+ Marley.joint 'basic_messaging',{:resources => ['message']}
7
+ module Marley
8
+ module Joints
9
+ class TaggedMessaging < Joint
10
+ def smoke
11
+ super
12
+ MR::Tag.tagging_for('PrivateMessage', 'User')
13
+ MR::Tag.tagging_for('Post', 'User')
14
+ MR::Tag.tagging_for('Post')
15
+ end
16
+ module ClassMethods
17
+ module Message
18
+ def list(params={})
19
+ if associations.include?(:public_tags)
20
+ specified_tags=params.delete(:tags)
21
+ specified_user_tags=params.delete(:user_tags)
22
+ else
23
+ specified_user_tags=params.delete(:tags)
24
+ end
25
+ tag_ids=MR::PublicTag.filter(:tag => specified_tags.split(/\s*,\s*/)).select(:id) if specified_tags
26
+ user_tag_ids=$request[:user].user_tags_dataset.filter(:tag => specified_user_tags.split(/\s*,\s*/)).select(:id) if specified_user_tags
27
+ items=filter(params)
28
+ #would love to make the following line more generic...
29
+ items=filter("author_id=#{$request[:user][:id]} or recipients like('%#{$request[:user][:name]}%')".lit) if new.rest_cols.include?(:recipients)
30
+ items=items.join(:messages_tags,:message_id => :id).filter(:tag_id => tag_ids) if specified_tags
31
+ items=items.join(:messages_tags,:message_id => :id).filter(:tag_id => user_tag_ids) if specified_user_tags
32
+ items.group(:thread_id).order(:max.sql_function(:date_created).desc,:max.sql_function(:date_updated).desc).map{|t|self[:parent_id => nil, :thread_id => t[:thread_id]].thread} rescue []
33
+ end
34
+ end
35
+ end
36
+ module InstanceMethods
37
+ module Message
38
+ def rest_associations
39
+ if ! new?
40
+ [ respond_to?(:public_tags) ? :public_tags : nil, respond_to?(:user_tags) ? user_tags_dataset.current_user_tags : nil].compact
41
+ end
42
+ end
43
+ def new_tags
44
+ [:instance,{:name => 'tags',:url => "#{url}tags", :new_rec => true, :schema => [['number','message_id',RESTRICT_HIDE,id],['text','tags',RESTRICT_REQ]]}]
45
+ end
46
+ def new_user_tags
47
+ [:instance,{:name => 'user_tags',:url => "#{url}user_tags", :new_rec => true, :schema => [['number','user_tags[message_id]',RESTRICT_HIDE,id],['text','user_tags[tags]',RESTRICT_REQ]]}]
48
+ end
49
+ def add_tags(tags,user=nil)
50
+ if respond_to?(:public_tags)
51
+ tags.to_s.split(',').each {|tag| add_public_tag(MR::PublicTag.find_or_create(:tag => tag))}
52
+ else
53
+ add_user_tags(tags,user)
54
+ end
55
+ end
56
+ def add_user_tags(tags,user=nil) #does not conflict with add_user_tag
57
+ user||=$request[:user][:id]
58
+ if user.class==String
59
+ user.split(',').each {|u| add_user_tags(tags,MR::User[:name => u][:id])}
60
+ elsif user.class==Array
61
+ user.each {|u| add_user_tags(tags,u)}
62
+ elsif user.class==Fixnum
63
+ tags.to_s.split(',').each {|tag| add_user_tag(MR::UserTag.find_or_create(:user_id => user, :tag => tag))}
64
+ end
65
+ end
66
+ end
67
+ end
68
+ module Resources
69
+ class User < MJ::BasicUser::Resources::User
70
+ end
71
+ class Admin < User
72
+ def self.requires_user?;true;end
73
+ end
74
+ class Moderator < User
75
+ def self.requires_user?;true;end
76
+ end
77
+ class PrivateMessage < MJ::BasicMessaging::Resources::PrivateMessage
78
+ attr_accessor :tags
79
+ @section_title='Private Messages'
80
+ @section_name='pms'
81
+ def self.section_navigation
82
+ $request[:user].user_tags.map{|t| [:link,{:url => "/private_message?private_message[tag]=#{t.tag}",:title => t.tag.humanize}]}.unshift(PrivateMessage.reggae_link('new'))
83
+ end
84
+ def get_actions; super << 'new_tags';end
85
+ def rest_schema
86
+ super << [:text, :tags, 0,tags]
87
+ end
88
+ def reply
89
+ r=super
90
+ r.tags=(user_tags_dataset.current_user_tags.map{|t|t.tag} - RESERVED_PM_TAGS).join(',')
91
+ r
92
+ end
93
+ def after_create
94
+ add_user_tags("inbox,#{tags}",recipients)
95
+ add_user_tags("sent,#{recipients.match(/\b#{author.name}\b/) ? '' : tags}",author_id)
96
+ end
97
+ end
98
+ class Post < MJ::BasicMessaging::Resources::Post
99
+ attr_accessor :tags,:my_tags
100
+ @section_title='Public Posts'
101
+ @section_name='posts'
102
+ def self.section_navigation
103
+ MR::Tag.filter(:user_id => nil).map{|t| [:link,{:url => "/post?post[tag]=#{t.tag}",:title => t.tag.humanize}]}.unshift([:link,{:url => '/post?post[untagged]=true',:title => 'Untagged Messages'}]).unshift(Post.reggae_link('new'))
104
+ end
105
+ def get_actions;(super << 'new_user_tags') << 'new_tags';end
106
+ def rest_schema
107
+ (super << [:text, :tags, 0,tags] ) << [:text, :my_tags, 0,my_tags]
108
+ end
109
+ def reply
110
+ r=super
111
+ r.tags=self.tags
112
+ r
113
+ end
114
+ def after_create
115
+ add_tags(tags) if tags
116
+ add_user_tags(my_tags) if my_tags
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,60 @@
1
+ module Marley
2
+ module Joints
3
+ class Tagging < Joint
4
+ module Resources
5
+ class Tag < Sequel::Model
6
+ def self.tagging_for(klass, user_class=nil,join_table=nil)
7
+ current_user_tags=Module.new do
8
+ def current_user_tags
9
+ filter(:tags__user_id => $request[:user][:id])
10
+ end
11
+ end
12
+ tagged_class=Marley::Resources.const_get(klass.to_sym)
13
+ join_table||=:"#{tagged_class.table_name}_tags"
14
+ klass_key=:"#{tagged_class.table_name.to_s.singularize}_id"
15
+ tag_key=:tag_id
16
+ attr_accessor klass_key
17
+ if user_class
18
+ UserTag.many_to_many klass.underscore.to_sym,:class => "Marley::Resources::#{klass}", :join_table => join_table,:left_key => tag_key,:right_key => klass_key,:extend => current_user_tags
19
+ tagged_class.many_to_many :user_tags, :class => 'Marley::Resources::UserTag',:join_table => join_table,:left_key => klass_key,:right_key => tag_key, :extend => current_user_tags
20
+ Marley::Resources.const_get(user_class).one_to_many :user_tags, :class => 'Marley::Resources::UserTag'
21
+ UserTag.many_to_one user_class.underscore.to_sym,:class => "Marley::Resources::#{user_class}"
22
+ else
23
+ PublicTag.many_to_many klass.underscore.to_sym,:class => "Marley::Resources::#{klass}", :join_table => join_table,:left_key => tag_key,:right_key => klass_key
24
+ tagged_class.many_to_many :public_tags,:class => "Marley::Resources::PublicTag",:join_table => join_table,:left_key => klass_key,:right_key => tag_key
25
+ end
26
+ end
27
+ def to_a
28
+ a=super
29
+ a[1][:delete_action]='remove_parent'
30
+ a
31
+ end
32
+ def validate
33
+ validates_presence :tag
34
+ validates_unique [:tag,:user_id]
35
+ end
36
+ def before_save
37
+ super
38
+ self.tag.downcase!
39
+ self.tag.strip!
40
+ end
41
+ def after_save
42
+ super
43
+ assoc=methods.grep(/_id=$/) - ['user_id=']
44
+ assoc.each do |a|
45
+ if c=self.send(a.sub(/=$/,''))
46
+ send "add_#{a.sub(/_id=/,'')}", Marley::Resources.const_get(a.sub(/_id=/,'').camelize.to_sym)[c]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ class PublicTag < Tag
52
+ set_dataset DB[:tags].filter(:user_id => nil)
53
+ end
54
+ class UserTag < Tag
55
+ set_dataset DB[:tags].filter(~{:user_id => nil})
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end