docfolio 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/docfolio.rb +2 -0
- data/lib/docfolio/date_format.rb +81 -0
- data/lib/docfolio/docfolio.rb +74 -0
- data/lib/docfolio/learning_diary.rb +87 -0
- data/lib/docfolio/paragraph.rb +270 -0
- data/lib/docfolio/tags.rb +158 -0
- data/lib/docfolio/views/collater_console_view.rb +14 -0
- data/lib/docfolio/views/diary_console_view.rb +228 -0
- data/lib/docfolio/views/view.rb +91 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 99d2c5789c65b55841fd11b71cfb29448e1ea722
|
4
|
+
data.tar.gz: d4221138fa97f72c806a8aaabd04f3881f0e4c76
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f6afcea04d4231186c8ac92f138637f75fa447073668603d83fbc9f285771d68e5fb83b7872d3466710a742751018063e52d8c4cfe389d69cd9a84fadb3e8bea
|
7
|
+
data.tar.gz: 27134b0e2c0d9151a33716307325fc8076a8994056776bef3c4abd197ce355e0e4d6e769a1d1d0373e7c61ed37bca1f2eb71062525ca1dddef86bb266c8e9a6b
|
data/lib/docfolio.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# convert the date from 'dd-Oct-yy' to seconds past UNIX epoc
|
2
|
+
# accepts dd_Mmm-yy dd-Mmmmmmm-yy dd-MMM-yy and other similar
|
3
|
+
module DateFormat
|
4
|
+
# Extracts a date in seconds past UNIX epoc from a string date. The result
|
5
|
+
# can be used for other date operations. Converts from dd-mmm-yy and similar
|
6
|
+
# formats as commonly found in csv files
|
7
|
+
class DateExtractor
|
8
|
+
def format_date(date)
|
9
|
+
day, month, year = components(date)
|
10
|
+
begin
|
11
|
+
Time.new(year, month, day).to_i
|
12
|
+
rescue ArgumentError => e
|
13
|
+
print_argument_error_msg(e)
|
14
|
+
return nil
|
15
|
+
rescue => e
|
16
|
+
raise e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def print_argument_error_msg(e)
|
23
|
+
puts "\n#{e.to_s.upcase}"
|
24
|
+
puts "date : #{date.inspect}"
|
25
|
+
puts "day : #{day}"
|
26
|
+
puts "month : #{month}"
|
27
|
+
puts "year : #{year}"
|
28
|
+
puts e.backtrace
|
29
|
+
end
|
30
|
+
|
31
|
+
# splits date into is component day month and time
|
32
|
+
def components(date)
|
33
|
+
date = date.split('-')
|
34
|
+
day = date[0].to_i
|
35
|
+
month = convert_month_to_number(date[1])
|
36
|
+
year = date[2].to_i
|
37
|
+
if year < 100 # no century
|
38
|
+
year > Time.now.year % 1000 ? century = 1900 : century = 2000
|
39
|
+
year += century
|
40
|
+
end
|
41
|
+
[day, month, year]
|
42
|
+
end
|
43
|
+
|
44
|
+
MONTHS = {
|
45
|
+
'jan' => 1,
|
46
|
+
'feb' => 2,
|
47
|
+
'mar' => 3,
|
48
|
+
'apr' => 4,
|
49
|
+
'may' => 5,
|
50
|
+
'jun' => 6,
|
51
|
+
'jul' => 7,
|
52
|
+
'aug' => 8,
|
53
|
+
'sep' => 9,
|
54
|
+
'oct' => 10,
|
55
|
+
'nov' => 11,
|
56
|
+
'dec' => 12,
|
57
|
+
'january' => 1,
|
58
|
+
'february' => 2,
|
59
|
+
'march' => 3,
|
60
|
+
'april' => 4,
|
61
|
+
'june' => 6,
|
62
|
+
'july' => 7,
|
63
|
+
'august' => 8,
|
64
|
+
'september' => 9,
|
65
|
+
'october' => 10,
|
66
|
+
'november' => 11,
|
67
|
+
'december' => 12,
|
68
|
+
'sept' => 9
|
69
|
+
}
|
70
|
+
|
71
|
+
def convert_month_to_number(month)
|
72
|
+
return month.to_i if month.to_i > 0 # already a number
|
73
|
+
month = month.downcase
|
74
|
+
MONTHS[month]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def format_date(date)
|
79
|
+
DateExtractor.new.format_date(date)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require_relative 'learning_diary.rb'
|
2
|
+
# require 'stringio'
|
3
|
+
require_relative 'views/collater_console_view.rb'
|
4
|
+
|
5
|
+
# collection class for Learning Diarys / logs
|
6
|
+
class Logs
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# iterates through every file in the data directory and parses with
|
10
|
+
# LearningDiary
|
11
|
+
def initialize(loading_logs = true)
|
12
|
+
@logs = []
|
13
|
+
if loading_logs
|
14
|
+
log_files_dir.each do |log_file_name|
|
15
|
+
@logs << LearningDiary.new(log_file_name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds either a file to logs or a directory to logs
|
21
|
+
# @param [String] file_or_directory A string containing the name of a
|
22
|
+
# file or directory
|
23
|
+
def add(file_or_directory)
|
24
|
+
log_directory(file_or_directory) if File.directory?(file_or_directory)
|
25
|
+
log_file(file_or_directory) if File.file?(file_or_directory)
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
@logs.each { |p| block.call(p) }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Adds a file to logs
|
35
|
+
# @param [String] file A string containing the name of a file
|
36
|
+
def log_file(file)
|
37
|
+
@logs << LearningDiary.new(file)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Adds a directory to logs
|
41
|
+
# @param [String] dir A string containing the name of a directory
|
42
|
+
def log_directory(dir)
|
43
|
+
Dir[dir + '/**/*.txt'].each { |file| log_file(file) }
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Array] An array of strings. Each string is a
|
47
|
+
# relative file path for every .txt file under the data directory
|
48
|
+
def log_files_dir
|
49
|
+
Dir['./docfolio/data/**/*.txt']
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# controller class for the diaries/logs collection
|
54
|
+
class Docfolio
|
55
|
+
def initialize
|
56
|
+
@logs = Logs.new
|
57
|
+
@view = CollaterConsoleView.new
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a portfolio
|
61
|
+
# @param [String] portfolio_file_or_directory A portfolio file or a
|
62
|
+
# name of a directory containing portfolio files including files
|
63
|
+
# within subdirectories.
|
64
|
+
def self.create(portfolio_file_or_directory)
|
65
|
+
logs = Logs.new(false)
|
66
|
+
logs.add(portfolio_file_or_directory)
|
67
|
+
CollaterConsoleView.new.print_logs(logs)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
def print_logs
|
72
|
+
@view.print_logs(@logs)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative 'date_format.rb'
|
2
|
+
require_relative 'paragraph.rb'
|
3
|
+
require_relative 'views/diary_console_view.rb'
|
4
|
+
|
5
|
+
# collection class for paragraphs
|
6
|
+
# synonym 'Topic' 'Learning Diary'
|
7
|
+
class LearningDiary
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
attr_reader :paragraphs
|
11
|
+
|
12
|
+
# @param [String] file name of a text file containing text in docfile DSL
|
13
|
+
def initialize(file)
|
14
|
+
# preparation
|
15
|
+
# @todo extract to an initialize_vars function, as in the paragraph class
|
16
|
+
Paragraph.reset
|
17
|
+
@console_view = DiaryConsoleView.new
|
18
|
+
@paragraphs = []
|
19
|
+
@standard_credits_array = []
|
20
|
+
@impact_credits_array = []
|
21
|
+
|
22
|
+
# read the whole txt file in one go
|
23
|
+
f = File.read(file, encoding: 'UTF-8')
|
24
|
+
|
25
|
+
# iterates through each paragraph
|
26
|
+
f.split(/\n/).each do |p|
|
27
|
+
next if p == '' # ignore if paragraph empty
|
28
|
+
@paragraphs << Paragraph.new(p) #
|
29
|
+
end
|
30
|
+
calc_standard_credits
|
31
|
+
calc_impact_credits
|
32
|
+
end
|
33
|
+
|
34
|
+
def each(&block)
|
35
|
+
@paragraphs.each { |p| block.call(p) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](index)
|
39
|
+
@paragraphs[index]
|
40
|
+
end
|
41
|
+
|
42
|
+
def standard_credits_total
|
43
|
+
@standard_credits_array.reduce(0) { |a, e| a + e[2] }
|
44
|
+
end
|
45
|
+
|
46
|
+
def standard_credits
|
47
|
+
@standard_credits_array
|
48
|
+
end
|
49
|
+
|
50
|
+
def impact_credits_total
|
51
|
+
@impact_credits_array.reduce(0) { |a, e| a + e[2] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def credits_total
|
55
|
+
impact_credits_total + standard_credits_total
|
56
|
+
end
|
57
|
+
|
58
|
+
def impact_credits
|
59
|
+
@impact_credits_array
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def calc_standard_credits
|
65
|
+
@paragraphs.each do |p|
|
66
|
+
next unless p.creditable?
|
67
|
+
start = p.start_time
|
68
|
+
finish = p.end_time
|
69
|
+
duration = p.period
|
70
|
+
@standard_credits_array << [start, finish, duration] unless duration == 0
|
71
|
+
end
|
72
|
+
@standard_credits_array.uniq!
|
73
|
+
end
|
74
|
+
|
75
|
+
def calc_impact_credits
|
76
|
+
|
77
|
+
@paragraphs.each do |p|
|
78
|
+
next unless p.impact_creditable?
|
79
|
+
# p p
|
80
|
+
start = p.start_time
|
81
|
+
finish = p.end_time
|
82
|
+
duration = p.period
|
83
|
+
@impact_credits_array << [start, finish, duration] unless duration == 0
|
84
|
+
end
|
85
|
+
@impact_credits_array.uniq!
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
require_relative 'tags.rb'
|
2
|
+
|
3
|
+
# @param [Array] time_array updated start and end times in hours and minutes
|
4
|
+
# @param [Array] times_and_dates current start and end times and date
|
5
|
+
# @return [Array] returns the new times_and_dates array for use going forwards
|
6
|
+
module MyTime
|
7
|
+
# processes new times and dates with current times and dates
|
8
|
+
class TimeProcesser
|
9
|
+
include Tags
|
10
|
+
|
11
|
+
# Takes class start end times and dates as Time objects and amends the
|
12
|
+
# times, advancing the date if the start time has crossed midnight.
|
13
|
+
# @param [Array] current_times_and_dates An array containing the
|
14
|
+
# Paragraph class instance variables for the start time, end time
|
15
|
+
# and date (day)
|
16
|
+
# @param [Array] new_times An array containing the from hour, from min,
|
17
|
+
# to hour, to min
|
18
|
+
# @return [Array] The updated Paragraph class instance variables for the
|
19
|
+
# start time, end time and date.
|
20
|
+
def process_times(new_times, current_times_and_dates)
|
21
|
+
@start_time, @end_time, @date = current_times_and_dates
|
22
|
+
f_hour, f_min, t_hour, t_min = new_times
|
23
|
+
if (has f_hour) && to_st_tme(f_hour, f_min)
|
24
|
+
t_hour = f_hour
|
25
|
+
t_min = f_min
|
26
|
+
end
|
27
|
+
to_end_time(t_hour, t_min) if has t_hour
|
28
|
+
[@start_time, @end_time, @date]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
|
34
|
+
# @param [Number] f_hour From hours
|
35
|
+
# @param [Number] f_min From minutes
|
36
|
+
# @return [Boolean] if has a start time and no end time return true else
|
37
|
+
# set start time and return false (the nil for the last assignment of
|
38
|
+
# start_t)
|
39
|
+
def to_st_tme(f_hour, f_min)
|
40
|
+
# treat the time as an end time if there is a start time and no end time
|
41
|
+
has(@start_time) && (!has @end_time) ? true : start_t(f_hour, f_min)
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_end_time(t_hour, t_min)
|
45
|
+
# extract_time_object simply adds hours and mins to date
|
46
|
+
@end_time = extract_time_object(t_hour, t_min, @date)
|
47
|
+
# if end_time before start_time, assume it is the following day
|
48
|
+
@end_time += a_day if @end_time < @start_time
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds hours and mins to date (Time). Then adds a day if the start time is
|
52
|
+
# before the end time. Finally, makes end time nil
|
53
|
+
# @todo why advance the start time by a day if before the end time?
|
54
|
+
# @todo why make end_time nil?
|
55
|
+
def start_t(hour, min)
|
56
|
+
# extract_time_object simply adds hours and mins to date
|
57
|
+
@start_time = extract_time_object(hour, min, @date)
|
58
|
+
|
59
|
+
# advance start time by one day if the start time is before the end_time
|
60
|
+
@start_time += a_day if (has @end_time) && @start_time < @end_time
|
61
|
+
@end_time = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# Improves readability of boolean condition statements. Is used from Time
|
65
|
+
# objects and integer hour component, but it works for any object
|
66
|
+
# @param [Object] time_date_component Any object
|
67
|
+
# @return [Boolean] true if the parameter is not nil
|
68
|
+
def has(time_date_component)
|
69
|
+
!time_date_component.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
# returns one day in seconds and adds a day to the @date
|
73
|
+
def a_day
|
74
|
+
@date.nil? ? fail('needs date') : @date += 86_400
|
75
|
+
86_400
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Takes class start end times and dates as Time objects and amends the
|
80
|
+
# times, advancing the date if the start time has crossed midnight.
|
81
|
+
# @param [Array] times_and_dates An array containing the Paragraph class
|
82
|
+
# instance variables for the start time, end time and date (day)
|
83
|
+
# @param [Array] time_array An array containing the from hour, from min,
|
84
|
+
# to hour, to min
|
85
|
+
def process_times(time_array, times_and_dates)
|
86
|
+
TimeProcesser.new.process_times(time_array, times_and_dates)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Used by LearningDiary
|
91
|
+
# Initialized with plain text, will parse and hold tagged content
|
92
|
+
# Can keep track of time, section and id information from previous paragraphs
|
93
|
+
# using class instance variables
|
94
|
+
class Paragraph
|
95
|
+
include Enumerable
|
96
|
+
include MyTime
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
include Tags
|
101
|
+
|
102
|
+
public
|
103
|
+
|
104
|
+
attr_reader :id, :tags
|
105
|
+
|
106
|
+
# instance start time and instance end time
|
107
|
+
attr_accessor :start_time, :end_time
|
108
|
+
|
109
|
+
# initialize class date, start and class end time class instance variables
|
110
|
+
@cst = @cet = @date = nil
|
111
|
+
@section = @id = 0 # :TITLE
|
112
|
+
|
113
|
+
class << self
|
114
|
+
# class starttime and class end time
|
115
|
+
attr_accessor :st, :et, :date, :section, :id
|
116
|
+
end
|
117
|
+
|
118
|
+
# @param [String] p a single paragraph from a text file.
|
119
|
+
def initialize(p)
|
120
|
+
# preparation
|
121
|
+
initialize_vars
|
122
|
+
|
123
|
+
# Extract the date and time from a paragraph if it contains date and time
|
124
|
+
# info. Removes the date and time from the paragraph puts whats left into
|
125
|
+
# rest_of_str. Puts the to hour, to min, from hour and from min into the
|
126
|
+
# time array. Puts the date into Paragraph.date as a Time object.
|
127
|
+
#
|
128
|
+
# Paragraph.date is a class instance variable that holds the date to apply
|
129
|
+
# to this and subsequent paragraphs. It is initialized to nil when the
|
130
|
+
# program starts and reset to nil when reset is called (which it is called
|
131
|
+
# by the LearningDiary when initializing to parse a new file, called by
|
132
|
+
# the Collater when iterating through each text file)
|
133
|
+
#
|
134
|
+
# The extract_date function is from the Tag module
|
135
|
+
rest_of_str, time_array, Paragraph.date = extract_date(p, Paragraph.date)
|
136
|
+
|
137
|
+
# if a date or time has been found (and extracted)
|
138
|
+
if rest_of_str != p
|
139
|
+
# transer class start and end times to those of this paragraph, reset
|
140
|
+
# section to :NOTE
|
141
|
+
note_time
|
142
|
+
|
143
|
+
# Takes the current class instance times and dates and newly extracted
|
144
|
+
# paragraph dates from this paragraph, follows a set of rules to
|
145
|
+
# determine what the class instant times and dates should become
|
146
|
+
assign_class_dates process_times(time_array, class_dates)
|
147
|
+
|
148
|
+
# tranfser class start and end times to those of this paragraph, reset
|
149
|
+
# section to :NOTE
|
150
|
+
note_time
|
151
|
+
end
|
152
|
+
|
153
|
+
# if a new date or time has not been found then return
|
154
|
+
# @todo should this be in an else statement?
|
155
|
+
return if rest_of_str == ''
|
156
|
+
|
157
|
+
tags_extracted?(rest_of_str) ? note_time : tag_section(rest_of_str)
|
158
|
+
end
|
159
|
+
|
160
|
+
# returns true if any tags are of type tag
|
161
|
+
# @param [Array] tag An array of tags
|
162
|
+
def tag?(tag)
|
163
|
+
@tags.each { |t| return true if t[0] == tag }
|
164
|
+
false
|
165
|
+
end
|
166
|
+
|
167
|
+
# resets the class variables so that a new file can be parsed
|
168
|
+
# is called by LearningDiary when preparing to parse a new txt file
|
169
|
+
def self.reset
|
170
|
+
Paragraph.date = Paragraph.st = Paragraph.et = nil
|
171
|
+
Paragraph.section = Paragraph.id = 0 # :TITLE
|
172
|
+
end
|
173
|
+
|
174
|
+
# @todo should this be private?
|
175
|
+
def initialize_vars
|
176
|
+
@date_specified = @end_time_specified = @start_time_specified = false
|
177
|
+
@start_time = @end_time = nil
|
178
|
+
@tags = []
|
179
|
+
@id = Paragraph.id
|
180
|
+
Paragraph.id += 1
|
181
|
+
end
|
182
|
+
|
183
|
+
# each on paragraph iterates through the tags
|
184
|
+
def each(&block)
|
185
|
+
@tags.each { |t| block.call(t) }
|
186
|
+
end
|
187
|
+
|
188
|
+
def [](index)
|
189
|
+
tags[index]
|
190
|
+
end
|
191
|
+
|
192
|
+
# true is the paragraph contains a tag that can earn credit
|
193
|
+
def creditable?
|
194
|
+
@tags.each { |t| return true if CREDITABLE.include?(t[0]) }
|
195
|
+
false
|
196
|
+
end
|
197
|
+
|
198
|
+
# true is the paragraph contains a tag that can earn impact credit
|
199
|
+
def impact_creditable?
|
200
|
+
@tags.each { |t| return true if t[0] == :I }
|
201
|
+
false
|
202
|
+
end
|
203
|
+
|
204
|
+
def period
|
205
|
+
return 0 if @end_time.nil? || @start_time.nil?
|
206
|
+
(@end_time - @start_time).to_i / 60
|
207
|
+
end
|
208
|
+
|
209
|
+
def latest_time
|
210
|
+
return @end_time unless @end_time.nil?
|
211
|
+
return @start_time unless @start_time.nil?
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
TAG = 0
|
218
|
+
CONTENT = 1
|
219
|
+
|
220
|
+
def assign_class_dates(array)
|
221
|
+
Paragraph.st, Paragraph.et, Paragraph.date = array
|
222
|
+
end
|
223
|
+
|
224
|
+
# @return [Array] An array containing the class instant variables for the
|
225
|
+
# start time, end time and the date.
|
226
|
+
def class_dates
|
227
|
+
[Paragraph.st, Paragraph.et, Paragraph.date]
|
228
|
+
end
|
229
|
+
|
230
|
+
def tags_extracted?(str)
|
231
|
+
(@tags.count) < (@tags += extract_tags(str)).count
|
232
|
+
end
|
233
|
+
|
234
|
+
def tag_section(str)
|
235
|
+
tag_it(SECTIONS[Paragraph.section], str)
|
236
|
+
end
|
237
|
+
|
238
|
+
def content(tag, str = '')
|
239
|
+
@tags.each { |t| str << t[CONTENT] + ' ' if t[TAG] == tag }
|
240
|
+
str
|
241
|
+
end
|
242
|
+
|
243
|
+
def next_section
|
244
|
+
Paragraph.section += 1 unless Paragraph.section == (SECTIONS.count - 1)
|
245
|
+
end
|
246
|
+
|
247
|
+
def tag_it(tag, p)
|
248
|
+
@tags << [tag, p]
|
249
|
+
next_section if Paragraph.section == 0 # :TITLE
|
250
|
+
end
|
251
|
+
|
252
|
+
def method_missing(n, *args, &block)
|
253
|
+
if args[0].nil? # tag getter
|
254
|
+
ALL_TAGS.include?(n) ? content(n) : super(n, *args, &block)
|
255
|
+
else # section setter
|
256
|
+
SECTIONS.include?(n) ? tag_it(n, args[0]) : super(n, *args, &block)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
|
261
|
+
# @todo this function does two things, split it up
|
262
|
+
# Sets the section to a simple :NOTE and transfers the class times to the
|
263
|
+
# instance times.
|
264
|
+
# new untagged sections should be of :NOTE (2)
|
265
|
+
def note_time
|
266
|
+
Paragraph.section = 2 #:NOTE
|
267
|
+
@start_time = Paragraph.st
|
268
|
+
@end_time = Paragraph.et
|
269
|
+
end
|
270
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require_relative 'date_format.rb'
|
2
|
+
require 'English'
|
3
|
+
|
4
|
+
# handles extraction of tagged or other significant content
|
5
|
+
module Tags
|
6
|
+
# interface: extract_date, extract_tags
|
7
|
+
TAGS = [:LP, :R, :DEN, :NOTE, :I] # recognized in text
|
8
|
+
CREDITABLE = [:LP, :R, :I] # can earn cpd credit
|
9
|
+
SECTIONS = [:TITLE, :INTRO, :NOTE] # assumed from position in document
|
10
|
+
SPECIAL = [:DATE] # internal tags with no meaning from content or position
|
11
|
+
ALL_TAGS = SECTIONS + TAGS + SPECIAL
|
12
|
+
|
13
|
+
# extracts and formats tags and pertaining text from a plain text paragraph
|
14
|
+
class TagFriend
|
15
|
+
include DateFormat
|
16
|
+
|
17
|
+
# @todo move function to one of the date or time handling classes
|
18
|
+
# The $LAST_MATCH_INFO global is equivalent to Rexexp.last_match and
|
19
|
+
# returns a MatchData object. This can be used as an array, where indices
|
20
|
+
# 1 - n are the matched backreferences of the last successful match
|
21
|
+
# @param [String] paragraph_text a paragraph from a DSL text file
|
22
|
+
# @param [Time] date of this paragraph. May be nil if not known.
|
23
|
+
# @return [Array<String, Array, Time>] Array of values to be returned
|
24
|
+
# [String return value] 'paragraph_text' the same paragraph that was passed to the function but without the matched date character if there were any.
|
25
|
+
# [Array return value] 'time_array' array of 4 integer representing the hours and minutes of the from and to times
|
26
|
+
# [Time return value] 'date' the date in (day month year) of this paragraph taken from the matched date_regex if there was one. Will be nil if there was no match and if the date passed to the function was also nil.
|
27
|
+
def extract_date(paragraph_text, date)
|
28
|
+
time_array = []
|
29
|
+
|
30
|
+
# if text contains a date match
|
31
|
+
if date_regex =~ paragraph_text
|
32
|
+
# $' (or $POSTMATCH), contains the characters after the match position
|
33
|
+
paragraph_text = $'
|
34
|
+
|
35
|
+
# strip whitespace if any remaining match or set to empty string
|
36
|
+
# if no match. If there is just white space after the match then
|
37
|
+
# this is truncated to an empty string
|
38
|
+
paragraph_text.nil? ? paragraph_text = '' : paragraph_text.strip!
|
39
|
+
|
40
|
+
# extracts the 'from' and 'to' times from the last match above. the
|
41
|
+
# time_array contains from_hour, from_min, to_hour, to_min, the
|
42
|
+
# date parameter is updated if the match found a new date
|
43
|
+
time_array, date = date_from_globals($LAST_MATCH_INFO, date)
|
44
|
+
end
|
45
|
+
[paragraph_text, time_array, date]
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_tags(paragraph_text)
|
49
|
+
tag_regex =~ paragraph_text ? extract_tag(paragraph_text) : []
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# @todo move function to one of the date or time handling classes
|
55
|
+
# returns a date from the 26 globals returned by date_regex
|
56
|
+
# @param [MatchData] glob_a the MatchData object return when the date_regex
|
57
|
+
# was matched to the paragraph
|
58
|
+
# @param [Time] date the date of the paragraph; may be nil if not known
|
59
|
+
# @return [Array] array of 4 integer representing the
|
60
|
+
# hours and minutes of the from and to times
|
61
|
+
# @return [Time] 'date' the date (day month year) of this paragraph
|
62
|
+
def date_from_globals(glob_a, date)
|
63
|
+
from_hour = glob([1, 23], glob_a)
|
64
|
+
from_min = glob([2, 24], glob_a)
|
65
|
+
to_hour = glob([3, 25], glob_a)
|
66
|
+
to_min = glob([4, 26], glob_a)
|
67
|
+
day = glob([5, 8, 12, 14, 17, 21], glob_a)
|
68
|
+
month = glob([6, 9, 11, 15, 18, 20], glob_a)
|
69
|
+
year = glob([7, 10, 13, 16, 19, 22], glob_a)
|
70
|
+
date = Time.at(format_date("#{day}-#{month}-#{year}")) unless day.nil?
|
71
|
+
[[from_hour, from_min, to_hour, to_min], date]
|
72
|
+
end
|
73
|
+
|
74
|
+
# @todo move function to one of the date or time handling classes
|
75
|
+
# Returns a regular expression to be used to match dates and times of
|
76
|
+
# the paragraph.
|
77
|
+
# @return [Regex] a regular expression to use to match dates and times
|
78
|
+
# in the paragraph
|
79
|
+
def date_regex
|
80
|
+
dy = /(?<day>\d{1,2})/
|
81
|
+
mt = /(?<month>\w+)/
|
82
|
+
yr = /(?<year>\d{2,4})/
|
83
|
+
time = /(?<hour>\d{1,2}):(?<min>\d{2})/
|
84
|
+
period = /#{time}( ?(?:-|to) ?#{time})?/
|
85
|
+
date1 = %r{#{dy}/#{dy}/#{yr}} # d/m/y
|
86
|
+
date2 = /#{dy},? #{mt},? #{yr}/ # d Month Year
|
87
|
+
date3 = /#{mt},? #{dy},? #{yr}/ # Month d Year
|
88
|
+
date = /#{date1}|#{date2}|#{date3}/
|
89
|
+
/^(#{period} ?#{date}?|#{date} ?#{period}?)/
|
90
|
+
end
|
91
|
+
|
92
|
+
# @todo would str be better with join?
|
93
|
+
# Creates a regex that can be used to match for tags that are recognized
|
94
|
+
# as part of the DSL, currently :LP, :R, :DEN, :NOTE and :I
|
95
|
+
def tag_regex
|
96
|
+
str = ''
|
97
|
+
TAGS.each do |t|
|
98
|
+
str << "#{t}|"
|
99
|
+
end
|
100
|
+
str.gsub!(/\|$/, '')
|
101
|
+
/\b(#{str}): ?/
|
102
|
+
end
|
103
|
+
|
104
|
+
# Extracts a particular parameter from the MatchData object return when the
|
105
|
+
# paragraph was matched with the date regex. Treats the MatchData
|
106
|
+
# as an array, iterating through each index represented in the i_a
|
107
|
+
# array to find and return a value if there is one.
|
108
|
+
# @param [Array] i_a Array of integers representing positions to test in
|
109
|
+
# array glob_a
|
110
|
+
# @param [MatchData] glob_a Array of matched backreferences of the last
|
111
|
+
# successful regular expression match
|
112
|
+
# @return the first element in MatchData that is not nil. Returns
|
113
|
+
# nil if there are no elements in MatchData at the indices in i_a that
|
114
|
+
# are not nil.
|
115
|
+
def glob(i_a, glob_a)
|
116
|
+
i_a.each { |n| return glob_a[n] unless glob_a[n].nil? }
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
|
120
|
+
def preface_with_note(a)
|
121
|
+
str = a[0].strip
|
122
|
+
str == '' ? [] : [[:NOTE, str]]
|
123
|
+
end
|
124
|
+
|
125
|
+
def tags_array(a)
|
126
|
+
tags = []
|
127
|
+
tag_count = (a.count - 1) / 2
|
128
|
+
1.upto(tag_count) do |i|
|
129
|
+
tag = a[(i * 2) - 1].to_sym
|
130
|
+
content = a[i * 2].strip
|
131
|
+
tags << [tag, content]
|
132
|
+
end
|
133
|
+
tags
|
134
|
+
end
|
135
|
+
|
136
|
+
def extract_tag(paragraph_text)
|
137
|
+
a = paragraph_text.split(tag_regex)
|
138
|
+
preface_with_note(a) + tags_array(a)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @param [Time] from time to which hours and minutes are added
|
143
|
+
# @param [Number] hour hours to add to from
|
144
|
+
# @param [Number] min minutes to add to from
|
145
|
+
# @return [Time] the result of hours and minutes after from
|
146
|
+
def extract_time_object(hour, min, from)
|
147
|
+
seconds = (hour.to_i * 3600) + (min.to_i * 60)
|
148
|
+
Time.at(from.to_i + seconds)
|
149
|
+
end
|
150
|
+
|
151
|
+
def extract_tags(paragraph_text)
|
152
|
+
TagFriend.new.extract_tags(paragraph_text)
|
153
|
+
end
|
154
|
+
|
155
|
+
def extract_date(paragraph_text, date)
|
156
|
+
TagFriend.new.extract_date(paragraph_text, date)
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Collates information from different logs and can output collated information
|
2
|
+
# for use in appraisals.
|
3
|
+
class CollaterConsoleView < ConsoleView
|
4
|
+
def print_logs(diaries)
|
5
|
+
diary_console_view = DiaryConsoleView.new
|
6
|
+
diaries.each do |diary|
|
7
|
+
str = diary_console_view.output(diary)
|
8
|
+
unless str == ''
|
9
|
+
puts str
|
10
|
+
puts "\n#{'=' * OUTPUT_WIDTH}\n\n"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require_relative 'view.rb'
|
3
|
+
|
4
|
+
# A console view class for the Learning Diary
|
5
|
+
class DiaryConsoleView < ConsoleView
|
6
|
+
def initialize(diary = nil)
|
7
|
+
return if diary.nil?
|
8
|
+
@paragraphs = diary.paragraphs
|
9
|
+
@credits = diary.standard_credits
|
10
|
+
@credits_total = diary.standard_credits_total
|
11
|
+
@impact = diary.impact_credits
|
12
|
+
@impact_total = diary.impact_credits_total
|
13
|
+
@total = diary.credits_total
|
14
|
+
end
|
15
|
+
|
16
|
+
def output(diary = nil)
|
17
|
+
initialize(diary)
|
18
|
+
output_str
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def output_str
|
24
|
+
str = ''
|
25
|
+
str += section_str('TITLE', content(:TITLE))
|
26
|
+
str += section_str('INTRODUCTION', content(:INTRO))
|
27
|
+
str += learning_str
|
28
|
+
str += credits_str
|
29
|
+
str
|
30
|
+
end
|
31
|
+
|
32
|
+
def learning_str(str = '')
|
33
|
+
str += section_str('LEARNING POINTS', content(:LP), true)
|
34
|
+
str += section_str('REFLECTION', reflection, true)
|
35
|
+
str += section_str('IMPACT', impact)
|
36
|
+
str += section_str('FURTHER STUDY', content(:DEN), true)
|
37
|
+
str += section_str('OTHER NOTES', content(:NOTE))
|
38
|
+
str
|
39
|
+
end
|
40
|
+
|
41
|
+
def credits_str(str = '')
|
42
|
+
str += section_str('CREDITS BREAKDOWN', credits_breakdown(@credits))
|
43
|
+
str += mins2hour(@credits_total) + "\n" unless @credits_total == 0
|
44
|
+
str += section_str('IMPACT BREAKDOWN', credits_breakdown(@impact))
|
45
|
+
str += mins2hour(@impact_total) + "\n" unless @impact_total == 0
|
46
|
+
str += section_str('TOTAL CREDITS CLAIMED', credits_total(@total))
|
47
|
+
str
|
48
|
+
end
|
49
|
+
|
50
|
+
def credits_total(total)
|
51
|
+
return '' if total == 0
|
52
|
+
total_line + mins2hour(total)
|
53
|
+
end
|
54
|
+
|
55
|
+
def content(tag)
|
56
|
+
arr = []
|
57
|
+
@paragraphs.each do |p|
|
58
|
+
if block_given? # yields the whole paragraph for further processing
|
59
|
+
arr = yield p if p.tag?(tag)
|
60
|
+
else # strip tag texts to an array of one text element per tag
|
61
|
+
text = p.send(tag).strip
|
62
|
+
arr << text unless text == ''
|
63
|
+
end
|
64
|
+
end
|
65
|
+
arr
|
66
|
+
end
|
67
|
+
|
68
|
+
def credits_breakdown(credits_array)
|
69
|
+
return '' if credits_array.empty?
|
70
|
+
str = ''
|
71
|
+
credit_count = credits_array.count - 1
|
72
|
+
0.upto(credit_count) do |i|
|
73
|
+
prev_credit, credit = elements(credits_array, i)
|
74
|
+
str << take_credit(prev_credit, credit, i)
|
75
|
+
end
|
76
|
+
str + sub_total_line
|
77
|
+
end
|
78
|
+
|
79
|
+
def reflection
|
80
|
+
arr = []
|
81
|
+
content(:R) do |p|
|
82
|
+
arr += reflections(p)
|
83
|
+
end
|
84
|
+
arr
|
85
|
+
end
|
86
|
+
|
87
|
+
def impact
|
88
|
+
arr = []
|
89
|
+
old_cr = []
|
90
|
+
date = ''
|
91
|
+
content(:I) { |p| old_cr = credit_line(p); break }
|
92
|
+
statement_a = []
|
93
|
+
content(:I) do |p|
|
94
|
+
date = strftime(p.start_time, '%e-%b-%y').underline
|
95
|
+
sta = impact_statement(p)
|
96
|
+
cr = credit_line(p)
|
97
|
+
if cr != old_cr # group claims of the same time/credit
|
98
|
+
arr << statement_line(date, statement_a, old_cr)
|
99
|
+
statement_a = []
|
100
|
+
statement_a << add_impact_statement(sta)
|
101
|
+
old_cr = cr
|
102
|
+
next
|
103
|
+
end
|
104
|
+
statement_a << add_impact_statement(sta)
|
105
|
+
end
|
106
|
+
line = statement_line(date, statement_a, old_cr)
|
107
|
+
arr << line unless line.nil?
|
108
|
+
arr
|
109
|
+
end
|
110
|
+
|
111
|
+
# @param [Array] e element - statement array from #impact_statement(paragraph) in
|
112
|
+
# the form [has_info?(Boolean), text(String)]
|
113
|
+
def add_impact_statement(e)
|
114
|
+
if e[0] == false
|
115
|
+
impact_err(e[1])
|
116
|
+
else
|
117
|
+
"\n\n#{e[1]}\n\n"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def statement_line(date, statement_a, old_cr)
|
122
|
+
return if statement_a.empty?
|
123
|
+
statement = statement_a.join
|
124
|
+
"\n#{date}#{statement}#{old_cr}".gsub(/\n\n\n\n/, "\n\n")
|
125
|
+
end
|
126
|
+
|
127
|
+
# @[param] p paragraph
|
128
|
+
def credit_line(p)
|
129
|
+
st = p.start_time
|
130
|
+
fin = p.end_time
|
131
|
+
amount = ((fin - st) / 60).to_i
|
132
|
+
cr = "Impact Credit Claimed: #{amount} mins"
|
133
|
+
cr += ' based on an original period of study and reflection from'
|
134
|
+
cr + " #{print_date_and_time(st)} to #{end_time(st, fin)}\n"
|
135
|
+
end
|
136
|
+
|
137
|
+
def impact_statement(paragraph)
|
138
|
+
arr = []
|
139
|
+
ref = rm_colour(reflections(paragraph).join(' '))
|
140
|
+
paragraph.each { |tag| arr << tag[1] if tag[0] == :I }
|
141
|
+
sta = arr.join(' ')
|
142
|
+
ref == '' ? [false, sta] : [true, "#{ref}\n\n#{sta}"]
|
143
|
+
end
|
144
|
+
|
145
|
+
def impact_err(sta)
|
146
|
+
ret = "#{sta}" + " Warning - this impact statement does"\
|
147
|
+
" not pertain to any learning points or reflection.".colorize(:red)
|
148
|
+
"\n\n#{ret}\n\n"
|
149
|
+
end
|
150
|
+
|
151
|
+
# @param [Array] paragraph A paragraph containing one or more reflections.
|
152
|
+
# The paragraph is an array of tags of the form [tag symbol, string]
|
153
|
+
# @return an array of reflections
|
154
|
+
# The reflection text is returned with preceeding learning points formatted
|
155
|
+
# in a different style. A learning point is attached to the reflection
|
156
|
+
# if it preceeds the reflection, is in the same paragraph and there are
|
157
|
+
# no other intervening reflections in the same paragraph i.e. Within the
|
158
|
+
# same paragraph, a reflection will take the preceeding LPs and then leave
|
159
|
+
# subsequent LPs which will be picked up by a subsequent reflection if
|
160
|
+
# there is one. Such successive reflections, within the same paragraph, can
|
161
|
+
# be placed in order and in context when reported.
|
162
|
+
def reflections(paragraph)
|
163
|
+
arr = []
|
164
|
+
lp = ''
|
165
|
+
paragraph.each do |tag|
|
166
|
+
if tag[0] == :R
|
167
|
+
arr << (lp + tag[1].colorize(:green))
|
168
|
+
lp = ''
|
169
|
+
end
|
170
|
+
lp += (tag[1] + ' ').colorize(:light_black) if tag[0] == :LP
|
171
|
+
end
|
172
|
+
arr
|
173
|
+
end
|
174
|
+
|
175
|
+
def on_same_day?(time1, time2)
|
176
|
+
one_day = 60 * 60 * 24
|
177
|
+
(time1.to_i / one_day).to_i == (time2.to_i / one_day).to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
# @return [Array] The previous and current elements in the array
|
181
|
+
def elements(a, i)
|
182
|
+
i > 0 ? [a[i - 1], a[i]] : [[], a[i]]
|
183
|
+
end
|
184
|
+
|
185
|
+
def strftime(t, format)
|
186
|
+
t.strftime(format)
|
187
|
+
end
|
188
|
+
|
189
|
+
def print_just_time(t)
|
190
|
+
strftime(t, '%H:%M')
|
191
|
+
end
|
192
|
+
|
193
|
+
def print_date_and_time(t)
|
194
|
+
strftime(t, '%e-%b-%y %H:%M')
|
195
|
+
end
|
196
|
+
|
197
|
+
def print_time_and_date(t)
|
198
|
+
strftime(t, '%H:%M (%-e-%b)')
|
199
|
+
end
|
200
|
+
|
201
|
+
def start_time(cr, prev_cr, i)
|
202
|
+
if i == 0 # first line always has the full date and time
|
203
|
+
print_date_and_time(cr)
|
204
|
+
else
|
205
|
+
on_same_day?(cr, prev_cr) ? print_just_time(cr) : print_date_and_time(cr)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def end_time(st, fin)
|
210
|
+
on_same_day?(st, fin) ? print_just_time(fin) : print_time_and_date(fin)
|
211
|
+
end
|
212
|
+
|
213
|
+
def credit_period(prev_credit, credit, i)
|
214
|
+
str = start_time(credit[0], prev_credit[0], i).rjust(16)
|
215
|
+
str << ' - '
|
216
|
+
str << end_time(credit[0], credit[1]).ljust(16)
|
217
|
+
end
|
218
|
+
|
219
|
+
def credit_amount(amount)
|
220
|
+
str = "#{amount}".rjust(4)
|
221
|
+
str << " mins\n"
|
222
|
+
end
|
223
|
+
|
224
|
+
def take_credit(prev_credit, credit, i)
|
225
|
+
str = credit_period(prev_credit, credit, i)
|
226
|
+
str << credit_amount(credit[2])
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# parent view class
|
2
|
+
class View
|
3
|
+
protected
|
4
|
+
|
5
|
+
def rm_colour(str)
|
6
|
+
str.gsub(/\e.*?m/, '')
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ConsoleView < View
|
11
|
+
protected
|
12
|
+
|
13
|
+
OUTPUT_WIDTH = 80
|
14
|
+
|
15
|
+
def print_array_section(title, body, list)
|
16
|
+
return '' if body.empty?
|
17
|
+
if list
|
18
|
+
to_console("#{title}") + "\n\n" +
|
19
|
+
to_console("• #{body.join("\n\n• ")}\n\n", 2, 4)
|
20
|
+
else
|
21
|
+
to_console("#{title}") +
|
22
|
+
to_console("#{body.join}\n\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def section_str(title, body = nil, list = false)
|
27
|
+
return '' if body.nil?
|
28
|
+
case body
|
29
|
+
when Array
|
30
|
+
return '' if body.empty?
|
31
|
+
print_array_section(title, body, list)
|
32
|
+
when String
|
33
|
+
return '' if body.strip == ''
|
34
|
+
"#{title}\n\n#{body}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def line(char)
|
39
|
+
' ' * 27 + char * 18 + "\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def sub_total_line
|
43
|
+
line('─')
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_line
|
47
|
+
line('═')
|
48
|
+
end
|
49
|
+
|
50
|
+
def mins2hour(mins)
|
51
|
+
hours = (mins.to_r / 60).to_i
|
52
|
+
mins -= hours * 60
|
53
|
+
hours == 1 ? h_str = 'hour' : h_str = 'hours'
|
54
|
+
mins == 1 ? m_str = 'min' : m_str = 'mins'
|
55
|
+
"#{hours}".rjust(30) + " #{h_str}".ljust(6) +
|
56
|
+
"#{mins.to_i}".rjust(3) + " #{m_str}\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
# prints the word to the console
|
60
|
+
def word2console(word, x_pos, margin)
|
61
|
+
str = ''
|
62
|
+
length = 1 + rm_colour(word).length
|
63
|
+
if x_pos + length >= OUTPUT_WIDTH
|
64
|
+
str += "\n" + (' ' * margin)
|
65
|
+
x_pos = margin + length
|
66
|
+
else
|
67
|
+
x_pos += length
|
68
|
+
end
|
69
|
+
str != '' ? debug = true : debug = false
|
70
|
+
str += word
|
71
|
+
[x_pos, str]
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_console(str, first_line_margin = 0, margin = 0)
|
75
|
+
return_str = ''
|
76
|
+
str.each_line do |line|
|
77
|
+
if line == "\n"
|
78
|
+
return_str += "\n\n"
|
79
|
+
next
|
80
|
+
end
|
81
|
+
return_str += ' ' * first_line_margin
|
82
|
+
x_pos = first_line_margin
|
83
|
+
words = line.split(' ')
|
84
|
+
words.each do |word|
|
85
|
+
x_pos, this_str = word2console(word, x_pos, margin)
|
86
|
+
return_str += this_str + ' '
|
87
|
+
end
|
88
|
+
end
|
89
|
+
return_str
|
90
|
+
end
|
91
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docfolio
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nick Bulmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-10 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: A domain specific language to aid recording of a personal learning portfolio
|
14
|
+
for UK General Practitioners.
|
15
|
+
email: n.bulmer@live.co.uk
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/docfolio.rb
|
21
|
+
- lib/docfolio/date_format.rb
|
22
|
+
- lib/docfolio/docfolio.rb
|
23
|
+
- lib/docfolio/learning_diary.rb
|
24
|
+
- lib/docfolio/paragraph.rb
|
25
|
+
- lib/docfolio/tags.rb
|
26
|
+
- lib/docfolio/views/collater_console_view.rb
|
27
|
+
- lib/docfolio/views/diary_console_view.rb
|
28
|
+
- lib/docfolio/views/view.rb
|
29
|
+
homepage: https://github.com/nickbulmer/docfolio
|
30
|
+
licenses:
|
31
|
+
- GNU
|
32
|
+
metadata: {}
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
require_paths:
|
36
|
+
- lib
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
requirements: []
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 2.4.8
|
50
|
+
signing_key:
|
51
|
+
specification_version: 4
|
52
|
+
summary: A DSL for GP CPD
|
53
|
+
test_files: []
|
54
|
+
has_rdoc:
|