place 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,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