place 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ module Place
2
+
3
+ # A TimePoint is a "named milestone"
4
+ class TimePoint < Ohm::Model
5
+ attribute :tid
6
+ index :tid
7
+ attribute :url
8
+
9
+ attribute :name
10
+ attribute :time
11
+ attribute :description
12
+
13
+ reference :project, Project
14
+
15
+ collection :workitems, WorkItem, :timepoint
16
+
17
+ class << self; attr_accessor :rules end
18
+ @rules = {
19
+ 'name' => '//time-point/field[@id="name"]',
20
+ 'description' => '//time-point/field[@id="description"]',
21
+ 'time' => '//time-point/field[@id="time"]'
22
+ }
23
+
24
+ # Collect all timepoints (url) under a given project url
25
+ def self.collect_all(base_url)
26
+ Dir.glob(base_url + '/tracker/timepoints/*.xml').each do |filename|
27
+ TimePoint.create(
28
+ :tid => File.basename(filename, '.xml'),
29
+ :url => filename,
30
+ :project => Project.find_by_url(base_url)
31
+ )
32
+ Place.logger.info("collected timepoint at #{filename}")
33
+ end
34
+ end
35
+
36
+ # Retrieve a specific timepoint data (name, time, description)
37
+ def retrieve
38
+ content = IO.readlines(url).join('')
39
+ doc = Nokogiri::XML(content)
40
+ self.class.rules.each_pair do |k,v|
41
+ tmp = doc.xpath(v)
42
+ self.send("#{k}=", tmp[0].content) unless tmp[0].nil?
43
+ end
44
+ Place.logger.info("retrieved timepoint #{self.url}")
45
+ self.save
46
+ end
47
+
48
+ def self.find_by_tid(tid)
49
+ return nil if tid.nil?
50
+ tps = self.find(:tid => tid.to_s)
51
+ tps.nil? ? nil : tps[0]
52
+ end
53
+ end
54
+
55
+ end
data/lib/place/user.rb ADDED
@@ -0,0 +1,79 @@
1
+ module Place
2
+
3
+ # A User is an account under Polarion
4
+ class User < Ohm::Model
5
+ attribute :name
6
+ index :name
7
+ attribute :url
8
+
9
+ attribute :full_name
10
+ attribute :description
11
+ attribute :email
12
+
13
+ collection :workrecords, WorkRecord, :user
14
+ collection :comments, Comment, :author
15
+
16
+ set :watches, WorkItem
17
+ collection :author_of, WorkItem, :author
18
+ set :assignments, WorkItem
19
+
20
+ collection :participations, Participation, :user
21
+
22
+
23
+ # Collect all users (name, url) under a given repository url
24
+ def self.collect_all(base_url)
25
+ Dir.glob(base_url + '/.polarion/user-management/users/**/user.xml').each do |filename|
26
+ User.create(
27
+ :name => File.dirname(filename).split(/\//).last,
28
+ :url => filename
29
+ )
30
+ Place.logger.info("collected user at #{filename}")
31
+ end
32
+ end
33
+
34
+ class << self; attr_accessor :rules end
35
+ @rules = {
36
+ 'full_name' => '//user/field[@id="name"]',
37
+ 'description' => '//user/field[@id="description"]',
38
+ 'email' => '//user/field[@id="email"]'
39
+ }
40
+
41
+ # Retrieve a specific user data (based on name and url only)
42
+ def retrieve
43
+ content = IO.readlines(url).join('')
44
+ doc = Nokogiri::XML(content)
45
+ self.class.rules.each_pair do |k,v|
46
+ tmp = doc.xpath(v)
47
+ self.send("#{k}=", tmp[0].content) unless tmp[0].nil?
48
+ end
49
+ Place.logger.info("retrieved user at #{self.name}")
50
+ self.save
51
+ end
52
+
53
+ def retrieve_watches
54
+ content = IO.readlines(url).join('')
55
+ doc = Nokogiri::XML(content)
56
+ doc.xpath('//user/field[@id="watches"]/list//item').each do |wa|
57
+ prj, wid = wa.text.split('$')
58
+ if Project.all.map{|p| p.name}.include?(prj)
59
+ wi = WorkItem.find_by_wid(wid)
60
+ unless wi.nil?
61
+ wi.watches << User.find_by_name(name)
62
+ wi.save
63
+ self.watches << wi
64
+ self.save
65
+ end
66
+ end
67
+ end
68
+ Place.logger.info("retrieved watches for user #{self.name}")
69
+ self
70
+ end
71
+
72
+ # Find a user by the given name, returns +nil+ if not present
73
+ def self.find_by_name(name)
74
+ users = self.find(:name => name.to_s)
75
+ users.nil? ? nil : users[0]
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1,179 @@
1
+
2
+ require 'set'
3
+
4
+ module Place
5
+
6
+ # Each WorkItem has a work-item id (wid) and an url that refers to its main xml file (workitem.xml) located under its own directory (that terminates with its Polarion ID = our wid).
7
+ class WorkItem < Ohm::Model
8
+ attribute :wid
9
+ index :wid
10
+
11
+ attribute :url
12
+ attribute :type
13
+ index :type
14
+ attribute :fields
15
+
16
+ reference :author, User
17
+ set :assignees, User
18
+
19
+ reference :timepoint, TimePoint
20
+ reference :project, Project
21
+
22
+ set :watches, User
23
+ collection :workrecords, WorkRecord, :workitem
24
+ collection :comments, Comment, :workitem
25
+
26
+ # Collect all workitems (wid and url) under a given project url
27
+ def self.collect_all(base_url)
28
+ Dir.glob(base_url + '/tracker/workitems/**/workitem.xml').each do |filename|
29
+ wi = WorkItem.create(
30
+ :wid => File.dirname(filename).split(/\//).last,
31
+ :url => filename,
32
+ :fields => "{}"
33
+ )
34
+ project_prefix = wi.wid.split(/\-/)[0]
35
+ wi.project = Project.find_by_prefix(project_prefix)
36
+ Place.logger.info("collected workitem at #{wi.url}")
37
+ wi.save
38
+ end
39
+ end
40
+
41
+ # Retrieve a specific workitem (based on wid and url only)
42
+ def retrieve
43
+ content = IO.readlines(url).join('')
44
+ doc = Nokogiri::XML(content)
45
+
46
+ doc.xpath('//work-item/field').each do |field|
47
+ self[field.attributes['id'].to_s] = field.content
48
+ end
49
+
50
+ self.type = self[:type]
51
+
52
+ self.author = User.find_by_name(self[:author])
53
+
54
+ unless self[:assignee].nil?
55
+ self[:assignee].split.each do |name|
56
+ u = User.find_by_name(name)
57
+ unless u.nil?
58
+ self.assignees << u
59
+ u.assignments << self
60
+ end
61
+ end
62
+ end
63
+
64
+ self.timepoint = TimePoint.find_by_tid(self['timePoint'])
65
+
66
+ %w(initialEstimate timeSpent remainingEstimate).each do |t|
67
+ self[t] = to_hours(self[t])
68
+ end
69
+
70
+ # links
71
+ doc.xpath('/work-item/field[@id="linkedWorkItems"]/list/struct').each do |struct|
72
+ l = Link.create(
73
+ :role => struct.xpath('item[@id="role"]').text,
74
+ :suspect => struct.xpath('item[@id="suspect"]').text == 'true' ? true : false,
75
+ :revision => struct.xpath('item[@id="revision"]').empty? ? false : true,
76
+ :from => self,
77
+ :to => WorkItem.find_by_wid(struct.xpath('item[@id="workItem"]').text),
78
+ :from_wid => self.wid,
79
+ :to_wid => struct.xpath('item[@id="workItem"]').text
80
+ )
81
+ end
82
+
83
+ # comments
84
+ Dir.glob(File.dirname(url) + '/comment-*.xml').each do |filename|
85
+ c = Comment.create(:url => filename)
86
+ c.retrieve
87
+ end
88
+
89
+ # workrecords
90
+ Dir.glob(File.dirname(url) + '/workrecord-*.xml').each do |filename|
91
+ wr = WorkRecord.create(:url => filename)
92
+ wr.retrieve
93
+ end
94
+
95
+ Place.logger.info("retrieved workitem #{self.wid}")
96
+
97
+ self.save
98
+ end
99
+
100
+ # Get value from specified field, +nil+ if not present
101
+ def [](fname)
102
+ JSON.parse(fields)[fname.to_s]
103
+ end
104
+
105
+ # Set value for specified field
106
+ def []=(fname, fvalue)
107
+ h = JSON.parse(self.fields)
108
+ h[fname.to_s] = fvalue
109
+ self.fields = h.to_json
110
+ end
111
+
112
+ # Returns a workitem by its wid, +nil+ if not present
113
+ def self.find_by_wid(wid)
114
+ wis = self.find(:wid => wid)
115
+ wis.nil? ? nil : wis[0]
116
+ end
117
+
118
+ # Returns all workitems of the given +type+
119
+ def self.find_by_type(type)
120
+ #self.all.select{|wi| wi.type == type.to_s}
121
+ self.find(:type => type.to_s)
122
+ end
123
+
124
+ # Returns all links from the current workitems to others, eventually only with a specified role
125
+ def links_out(role=nil)
126
+ if role.nil?
127
+ Link.find(:from_wid => self.wid)
128
+ else
129
+ Link.find(:from_wid => self.wid, :role => role.to_s)
130
+ end
131
+ end
132
+
133
+ # Returns all links to the current workitems from others, eventually only with a specified role
134
+ def links_in(role=nil)
135
+ if role.nil?
136
+ Link.find(:to_wid => self.wid)
137
+ else
138
+ Link.find(:to_wid => self.wid, :role => role.to_s)
139
+ end
140
+ end
141
+
142
+ # Returns all the fields name for the workitem
143
+ def field_names
144
+ JSON.parse(self.fields).keys.sort
145
+ end
146
+
147
+ # Returns the total sum, for a given +field+, from descendant and workitem itself
148
+ # Note: descendants = children with parent role
149
+ def total_on(field)
150
+ # return a past computed value if present
151
+ return self["_total_on_#{field}"] unless self["_total_on_#{field}"].nil?
152
+
153
+ # computing the total
154
+ total = self[field].nil? ? 0 : self[field]
155
+ children.each{|c| total += c.total_on(field) }
156
+
157
+ # saving (momoizing) for future invocations
158
+ self["_total_on_#{field}"] = total.to_f
159
+ self.save
160
+
161
+ total.to_f
162
+ end
163
+
164
+
165
+ private
166
+
167
+
168
+ # Returns workitem effective children (linked in with a parent role)
169
+ def children
170
+ tmp = [].to_set
171
+ project.parent_roles.each do |role|
172
+ tmp.merge(links_in(role).map{|lnk| lnk.from}) unless links_in(role).empty?
173
+ end
174
+ tmp.to_a
175
+ end
176
+
177
+ end
178
+
179
+ end
@@ -0,0 +1,51 @@
1
+ module Place
2
+
3
+ # Each WorkRecord ties a User to a WorkItem, with a +time_spent+, +date+ plus a +type+ and a +comment+
4
+ class WorkRecord < Ohm::Model
5
+ attribute :url
6
+ index :url
7
+
8
+ attribute :time_spent
9
+ attribute :date
10
+ attribute :type
11
+ attribute :comment
12
+
13
+ reference :user, User
14
+ reference :workitem, WorkItem
15
+
16
+ class << self; attr_accessor :rules end
17
+ @rules = {
18
+ 'time_spent' => '//work-record/field[@id="timeSpent"]',
19
+ 'date' => '//work-record/field[@id="date"]',
20
+ 'type' => '//work-record/field[@id="type"]',
21
+ 'comment' => '//work-record/field[@id="comment"]',
22
+ 'user' => '//work-record/field[@id="user"]'
23
+ }
24
+
25
+ # Retrieve a specific workrecord data (time_spent, date, user, workitem) + (type, comment)
26
+ def retrieve
27
+ content = IO.readlines(url).join('')
28
+ doc = Nokogiri::XML(content)
29
+ self.class.rules.each_pair do |k,v|
30
+ tmp = doc.xpath(v)
31
+
32
+ case k
33
+ when 'time_spent'
34
+ self.send("#{k}=", to_hours(tmp[0].content))
35
+ when 'user'
36
+ self.send("#{k}=", User.find_by_name(tmp[0].content))
37
+ else
38
+ self.send("#{k}=", tmp[0].content) unless tmp[0].nil?
39
+ end
40
+ end
41
+
42
+ self.workitem = WorkItem.find_by_wid(File.dirname(self.url).split(/\//).last)
43
+
44
+ Place.logger.info("saved workrecord #{self.url}")
45
+
46
+ self.save
47
+ end
48
+ end
49
+
50
+
51
+ end
data/lib/place.rb ADDED
@@ -0,0 +1,112 @@
1
+ require 'rubygems'
2
+
3
+ require 'ohm'
4
+ require 'nokogiri'
5
+ require 'json'
6
+ require 'logger'
7
+
8
+
9
+ require File.join(File.dirname(__FILE__), 'place', 'project')
10
+ require File.join(File.dirname(__FILE__), 'place', 'time_point')
11
+ require File.join(File.dirname(__FILE__), 'place', 'user')
12
+ require File.join(File.dirname(__FILE__), 'place', 'work_item')
13
+ require File.join(File.dirname(__FILE__), 'place', 'work_record')
14
+ require File.join(File.dirname(__FILE__), 'place', 'comment')
15
+ require File.join(File.dirname(__FILE__), 'place', 'link')
16
+ require File.join(File.dirname(__FILE__), 'place', 'participation')
17
+
18
+
19
+ module Place
20
+ VERSION = '0.1.0'
21
+
22
+ @conf = {}
23
+
24
+ def self.setup(repository_path)
25
+ @conf['base'] = repository_path
26
+ Ohm.connect
27
+ end
28
+
29
+ def self.conf
30
+ @conf
31
+ end
32
+
33
+ def self.logger
34
+ @log
35
+ end
36
+
37
+ def self.logger_setup(filename=nil)
38
+ unless filename.nil?
39
+ @log = Logger.new(filename)
40
+ else
41
+ @log = Logger.new(STDERR)
42
+ end
43
+ @log.level = Logger::INFO
44
+ end
45
+
46
+ # Collect and retrieve all repo information, for each contained project
47
+ # If a project url is specified, it retrieves only info on this project
48
+ # BEWARE: each gather flush the whole database!
49
+ def gather!(project_url=nil)
50
+ start_time = Time.now.to_i
51
+
52
+ path = @conf['base'] # without ending '.polarion'
53
+ Ohm.flush
54
+
55
+ Project.collect_all(path)
56
+ User.collect_all(path)
57
+
58
+ unless project_url.nil?
59
+ prj = Project.find_by_url(project_url)
60
+ prj.retrieve
61
+ else
62
+ Project.all.each{|prj| prj.retrieve}
63
+ end
64
+
65
+ Ohm.redis.set 'updated_duration', Time.now.to_i - start_time
66
+ end
67
+
68
+ # Write last update timespent for the whole database
69
+ def updated!
70
+ Ohm.redis.set 'updated', Time.now.strftime("%d.%m.%Y %H:%M:%S")
71
+ end
72
+
73
+ def updated_at
74
+ Ohm.redis.get 'updated'
75
+ end
76
+
77
+ def updated?
78
+ not updated_at.nil?
79
+ end
80
+
81
+ def updated_duration
82
+ Ohm.redis.get 'updated_duration'
83
+ end
84
+
85
+ def reset!
86
+ Ohm.flush
87
+ end
88
+
89
+ # from duration to number of hours (1d == 8h)
90
+ def to_hours(duration)
91
+ return nil if duration.nil?
92
+ # TODO with a case statement or another regexp?
93
+ if duration =~ /(\d+)d\s(\d+)h/
94
+ days, hours = $1.to_f, $2.to_f
95
+ elsif duration =~ /(\d+)\/(\d+)d/
96
+ days = $1.to_f / $2.to_f
97
+ hours = 0
98
+ elsif duration =~ /(\d+)\/(\d+)h/
99
+ days = 0
100
+ hours = $1.to_f / $2.to_f
101
+ elsif duration =~ /(\d+)d/
102
+ days, hours = $1.to_f, 0
103
+ elsif duration =~ /(\d+)h/
104
+ days, hours = 0, $1.to_f
105
+ else
106
+ days, hours = 0, 0
107
+ end
108
+
109
+ (8*days + hours)
110
+ end
111
+
112
+ end
data/place.gemspec ADDED
@@ -0,0 +1,19 @@
1
+
2
+ Gem::Specification.new do |s|
3
+ s.name = "place"
4
+ s.version = "0.1.0"
5
+ s.summary = "Polarion LACE: Polarion items access in Ruby"
6
+ s.description = <<-EOF
7
+ Extract information from Polarion and turns it into easy manageable ruby objects.
8
+
9
+ Those objects are stored via Redis.
10
+ EOF
11
+ s.authors = ["Carlo Pecchia"]
12
+ s.email = ["info@carlopecchia.eu"]
13
+ s.homepage = "http://github.com/carlopecchia/place"
14
+ s.add_dependency('json')
15
+ s.add_dependency('ohm')
16
+ s.add_dependency('nokogiri')
17
+ s.files = ["Changelog", "LICENSE", "README.markdown", "Technical.markdown", "Rakefile", "lib/place.rb", "lib/place/project.rb", "lib/place/link.rb", "lib/place/participation.rb", "lib/place/user.rb", "lib/place/time_point.rb", "lib/place/work_item.rb", "lib/place/work_record.rb", "lib/place/comment.rb", "place.gemspec", "test/test_project.rb", "test/helper.rb", "test/test_workitem.rb", "test/test_time_point.rb", "test/test_place.rb", "test/test_comment.rb", "test/test_user.rb", "examples/sample-1.rb"]
18
+ end
19
+
data/test/helper.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $: << File.join(File.dirname(__FILE__), '../lib')
5
+ require 'ohm'
6
+ require 'place'
7
+ include Place
8
+
9
+ Place.logger_setup('/dev/null')
@@ -0,0 +1,35 @@
1
+
2
+ $: << File.join(File.dirname(__FILE__), '.')
3
+ require 'helper.rb'
4
+
5
+ class CommentTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ Place.setup(File.expand_path(File.join(File.dirname(__FILE__), 'repo')))
9
+ Place.gather!
10
+ end
11
+
12
+ def teardown
13
+ Ohm.flush
14
+ end
15
+
16
+ def test_environment
17
+ Comment.all.each do |c|
18
+ assert_not_nil c.author
19
+ assert_not_nil c.workitem
20
+ assert_not_nil c.text
21
+ assert_not_nil c.created
22
+ end
23
+ end
24
+
25
+ def test_find_by_cid
26
+ assert_not_nil Comment.find_by_cid('P-6#1')
27
+ assert_nil Comment.find_by_cid('P-6#z00')
28
+ end
29
+
30
+ def test_parent
31
+ c = Comment.find_by_cid('P-6#2')
32
+ assert_not_nil c.parent
33
+ assert_equal Comment.find_by_cid('P-6#1'), c.parent
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+
2
+ $: << File.join(File.dirname(__FILE__), '.')
3
+ require 'helper.rb'
4
+
5
+ class PlaceTest < Test::Unit::TestCase
6
+
7
+ def teardown
8
+ Ohm.flush
9
+ end
10
+
11
+ def test_gather
12
+ base = File.expand_path(File.join(File.dirname(__FILE__), 'repo'))
13
+ Place.setup(base)
14
+ Place.gather!
15
+
16
+ assert_equal 2, Project.all.size
17
+ end
18
+
19
+ def test_duration
20
+ assert_equal 11, to_hours('1d 3h')
21
+ assert_equal 16, to_hours('2d')
22
+ assert_equal 3, to_hours('3h')
23
+ assert_equal 0.5, to_hours('1/2h')
24
+ assert_equal 4, to_hours('1/2d')
25
+ assert_equal 0, to_hours('')
26
+ assert_equal nil, to_hours(nil)
27
+ end
28
+ end
@@ -0,0 +1,100 @@
1
+
2
+ $: << File.join(File.dirname(__FILE__), '.')
3
+ require 'helper.rb'
4
+
5
+ class ProjectTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ @base = File.expand_path(File.join(File.dirname(__FILE__), 'repo'))
9
+ Place.setup(@base)
10
+ Place.gather!
11
+ end
12
+
13
+ def teardown
14
+ Ohm.flush
15
+ end
16
+
17
+ def test_collecting
18
+ assert_not_nil @base
19
+ assert_equal 2, Project.all.size
20
+ end
21
+
22
+ def test_find_by_name
23
+ p = Project.find_by_name('P')
24
+ assert_equal 'P', p.name
25
+ assert_equal 'P', p.prefix
26
+ assert_not_nil p.description
27
+
28
+ p = Project.find_by_name 'NONE'
29
+ assert_nil p
30
+ end
31
+
32
+ def test_find_by_url
33
+ p = Project.find_by_url(@base + '/P/.polarion')
34
+ assert_not_nil p
35
+
36
+ b = Project.find_by_url(@base + '/A/B/.polarion')
37
+ assert_not_nil b
38
+
39
+ n = Project.find_by_url(@base + '/none/.polarion')
40
+ assert_nil n
41
+ end
42
+
43
+ def test_find_by_prefix
44
+ p = Project.find_by_prefix('P')
45
+ assert_not_nil p
46
+
47
+ p = Project.find_by_prefix('NONE')
48
+ assert_nil p
49
+ end
50
+
51
+ def test_workitems
52
+ p = Project.find_by_name('P')
53
+ assert_not_nil p
54
+
55
+ assert p.workitems.all.size > 0
56
+
57
+ wi = p.workitems.all.first
58
+ assert_not_nil wi
59
+ assert_equal p, wi.project
60
+ end
61
+
62
+ def test_roles
63
+ p = Project.find_by_name('P')
64
+ assert_not_nil p
65
+
66
+ assert p.roles.size > 0
67
+ assert p.roles.include?('project_admin')
68
+ assert !p.roles.include?('foo servant role')
69
+ end
70
+
71
+ def test_users
72
+ p = Project.find_by_name('P')
73
+ assert_not_nil p
74
+
75
+ assert_equal 2, p.users.size
76
+ assert_equal 1, p.users('project_admin').size
77
+
78
+ u = User.find_by_name 'alice'
79
+ assert_not_nil u
80
+ assert_equal u, p.users('project_admin').first
81
+ end
82
+
83
+ def test_parent_roles
84
+ p = Project.find_by_name('P')
85
+ assert_not_nil p
86
+
87
+ assert ! p.parent_roles.empty?
88
+ assert p.parent_roles.include?('parent')
89
+ assert p.parent_roles.include?('refines')
90
+ assert ! p.parent_roles.include?('depends')
91
+
92
+ # A/B doesn't have a local file for workitem-link-roles
93
+ b = Project.find_by_url(@base + '/A/B/.polarion')
94
+ assert_not_nil b
95
+ assert ! b.parent_roles.empty?
96
+ assert b.parent_roles.include?('parent')
97
+ assert_equal 1, b.parent_roles.size
98
+ end
99
+
100
+ end