docfolio 0.0.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.
- 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:
|