docfolio 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ require_relative 'docfolio/docfolio.rb'
2
+ Docfolio.new.print_logs
@@ -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: