feat(BREAKING): limit shown employment history
Allow users to limit the amount of employment history shown in the PDF document by specifying a time range. This is a breaking change as the structure of the CV needs to slightly change. The employment's start and end dates need to be represented as integers. Additional refactoring: - The CV type is now in the internal cv package. - The template functions are now in the internal templateFuncs package.
This commit is contained in:
parent
76fbe5c704
commit
fa8dbe68e9
13 changed files with 248 additions and 117 deletions
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This project parses a CV, written in JSON and generates it as a PDF document.
|
Spruce is a tool that generates a PDF document from a CV written in JSON.
|
||||||
The PDF generation is completed in two steps:
|
The PDF generation is completed in two steps:
|
||||||
|
|
||||||
1. The application parses the JSON document and generates a TEX file using the TEX template files located in the [templates](./templates) directory.
|
1. The application parses the JSON document and generates a TEX file using the TEX template files located in the [templates](./templates) directory.
|
||||||
|
@ -14,7 +14,7 @@ If you are interested in generating your own CV, then below is a list of depende
|
||||||
|
|
||||||
- **Go:** Please go [here](https://go.dev/dl/) to download the latest version of the Go programming language.
|
- **Go:** Please go [here](https://go.dev/dl/) to download the latest version of the Go programming language.
|
||||||
- **ConTeXt:** You can go to the [installation page](https://wiki.contextgarden.net/Installation) to find out how to install ConTeXt for your Operating System.
|
- **ConTeXt:** You can go to the [installation page](https://wiki.contextgarden.net/Installation) to find out how to install ConTeXt for your Operating System.
|
||||||
- **The Carlito font (ttf-carlito):** In previous iterations of my CV I used the Calibri font. Carlito is the free, metric compatible alternative to this and is specified in the TEX template.
|
- **The Carlito font (ttf-carlito):** A free, metric compatible alternative to the Calibri.
|
||||||
- For Ubuntu/Debian installation you can use `apt`:
|
- For Ubuntu/Debian installation you can use `apt`:
|
||||||
```bash
|
```bash
|
||||||
$ apt install font-crosextra-carlito
|
$ apt install font-crosextra-carlito
|
||||||
|
|
46
cv.go
46
cv.go
|
@ -1,46 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
type Cv struct {
|
|
||||||
FirstName string `json:"firstName"`
|
|
||||||
LastName string `json:"lastName"`
|
|
||||||
JobTitle string `json:"jobTitle"`
|
|
||||||
Contact map[string]string `json:"contact"`
|
|
||||||
Links map[string]string `json:"links"`
|
|
||||||
Summary []string `json:"summary"`
|
|
||||||
Skills []Skills `json:"skills"`
|
|
||||||
Employment []Experience `json:"employment"`
|
|
||||||
Education []Experience `json:"education"`
|
|
||||||
Interests []string `json:"interests"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Contact struct {
|
|
||||||
Email string `json:"email"`
|
|
||||||
Phone string `json:"phone"`
|
|
||||||
Location string `json:"location"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Skills struct {
|
|
||||||
Category string `json:"category"`
|
|
||||||
Values []string `json:"values"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Experience struct {
|
|
||||||
Company string `json:"company,omitempty"`
|
|
||||||
School string `json:"school,omitempty"`
|
|
||||||
Location string `json:"location"`
|
|
||||||
JobTitle string `json:"jobTitle,omitempty"`
|
|
||||||
Qualification string `json:"qualification,omitempty"`
|
|
||||||
Duration Duration `json:"duration"`
|
|
||||||
Details []string `json:"details"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Duration struct {
|
|
||||||
Start Date `json:"start"`
|
|
||||||
End Date `json:"end"`
|
|
||||||
Present string `json:"present"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Date struct {
|
|
||||||
Year string `json:"year"`
|
|
||||||
Month string `json:"month"`
|
|
||||||
}
|
|
|
@ -45,10 +45,11 @@
|
||||||
"jobTitle": "Software Engineer (Cloud Platform)",
|
"jobTitle": "Software Engineer (Cloud Platform)",
|
||||||
"duration": {
|
"duration": {
|
||||||
"start": {
|
"start": {
|
||||||
"year": "2020",
|
"year": 2020,
|
||||||
"month": "August"
|
"month": 4,
|
||||||
|
"day": 5
|
||||||
},
|
},
|
||||||
"present": "yes"
|
"present": true
|
||||||
},
|
},
|
||||||
"details": [
|
"details": [
|
||||||
"Est doloremque molestiae ipsum id occaecati porro repudiandae corporis.",
|
"Est doloremque molestiae ipsum id occaecati porro repudiandae corporis.",
|
||||||
|
@ -62,12 +63,14 @@
|
||||||
"jobTitle": "Software Engineer (Backend)",
|
"jobTitle": "Software Engineer (Backend)",
|
||||||
"duration": {
|
"duration": {
|
||||||
"start": {
|
"start": {
|
||||||
"year": "2015",
|
"year": 2015,
|
||||||
"month": "September"
|
"month": 9,
|
||||||
|
"day": 3
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"year": "2021",
|
"year": 2021,
|
||||||
"month": "August"
|
"month": 8,
|
||||||
|
"day": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"details": [
|
"details": [
|
||||||
|
@ -82,12 +85,14 @@
|
||||||
"jobTitle": "Software Engineer (Backend)",
|
"jobTitle": "Software Engineer (Backend)",
|
||||||
"duration": {
|
"duration": {
|
||||||
"start": {
|
"start": {
|
||||||
"year": "2010",
|
"year": 2010,
|
||||||
"month": "January"
|
"month": 9,
|
||||||
|
"day": 3
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"year": "2015",
|
"year": 2015,
|
||||||
"month": "September"
|
"month": 9,
|
||||||
|
"day": 17
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"details": [
|
"details": [
|
||||||
|
@ -95,21 +100,45 @@
|
||||||
"Animi et sed nostrum, inventore debitis, quia voluptates dolor cum tempore vero commodi.",
|
"Animi et sed nostrum, inventore debitis, quia voluptates dolor cum tempore vero commodi.",
|
||||||
"Distinctio sequi commodi debitis enim voluptas ex ipsum."
|
"Distinctio sequi commodi debitis enim voluptas ex ipsum."
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"company": "Engineering Department, Example University",
|
||||||
|
"location": "London (Onsite)",
|
||||||
|
"jobTitle": "Research and Development Assistant",
|
||||||
|
"duration": {
|
||||||
|
"start": {
|
||||||
|
"year": 2009,
|
||||||
|
"month": 11,
|
||||||
|
"day": 13
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"year": 2010,
|
||||||
|
"month": 7,
|
||||||
|
"day": 30
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"details": [
|
||||||
|
"Harum voluptas labore impedit itaque animi repellendus dolorem, Impedit error esse hic at ea ut ab ipsam.",
|
||||||
|
"Repudiandae sed architecto quia temporibus eum corrupti.",
|
||||||
|
"Est libero consectetur laborum possimus, Deserunt ea pariatur quia veniam expedita facilis quis."
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"education": [
|
"education": [
|
||||||
{
|
{
|
||||||
"school": "Example University",
|
"school": "Example University",
|
||||||
"location": "London",
|
"location": "London",
|
||||||
"qualification": "BSc (Hons) in Computer Science & Artificial Intelligence, 1st",
|
"qualification": "BSc (Hons) in Computer Science & Artificial Intelligence, 1st Class",
|
||||||
"duration": {
|
"duration": {
|
||||||
"start": {
|
"start": {
|
||||||
"year": "2012",
|
"year": 2007,
|
||||||
"month": "October"
|
"month": 10,
|
||||||
|
"day": 4
|
||||||
},
|
},
|
||||||
"end": {
|
"end": {
|
||||||
"year": "2015",
|
"year": 2010,
|
||||||
"month": "July"
|
"month": 7,
|
||||||
|
"day": 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,3 @@
|
||||||
module codeflow.dananglin.me.uk/apollo/spruce
|
module codeflow.dananglin.me.uk/apollo/spruce
|
||||||
|
|
||||||
go 1.19
|
go 1.20
|
||||||
|
|
25
internal/cv/cv.go
Normal file
25
internal/cv/cv.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package cv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCV returns a new CV value from the given JSON file.
|
||||||
|
func NewCV(path string) (CV, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return CV{}, fmt.Errorf("unable to open %s; %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
|
||||||
|
var c CV
|
||||||
|
|
||||||
|
if err = decoder.Decode(&c); err != nil {
|
||||||
|
return CV{}, fmt.Errorf("unable to decode JSON data; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
67
internal/cv/types.go
Normal file
67
internal/cv/types.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package cv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CV struct {
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
JobTitle string `json:"jobTitle"`
|
||||||
|
Contact map[string]string `json:"contact"`
|
||||||
|
Links map[string]string `json:"links"`
|
||||||
|
Summary []string `json:"summary"`
|
||||||
|
Skills []Skills `json:"skills"`
|
||||||
|
Employment []Experience `json:"employment"`
|
||||||
|
Education []Experience `json:"education"`
|
||||||
|
Interests []string `json:"interests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Skills struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
Values []string `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Experience struct {
|
||||||
|
Company string `json:"company"`
|
||||||
|
School string `json:"school"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
JobTitle string `json:"jobTitle"`
|
||||||
|
Qualification string `json:"qualification"`
|
||||||
|
Duration Duration `json:"duration"`
|
||||||
|
Details []string `json:"details"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Duration struct {
|
||||||
|
Start Date `json:"start"`
|
||||||
|
End Date `json:"end"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) After(t time.Time) (bool, error) {
|
||||||
|
endDate, err := d.End.ParseDate()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return endDate.After(t), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type Date struct {
|
||||||
|
Year int `json:"year"`
|
||||||
|
Month int `json:"month"`
|
||||||
|
Day int `json:"day"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Date) ParseDate() (time.Time, error) {
|
||||||
|
dateStr := fmt.Sprintf("%d-%02d-%02d", d.Year, d.Month, d.Day)
|
||||||
|
|
||||||
|
date, err := time.Parse(time.DateOnly, dateStr)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("unable to parse the date; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return date, nil
|
||||||
|
}
|
36
internal/templateFuncs/formatduration.go
Normal file
36
internal/templateFuncs/formatduration.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatDuration outputs the employment/education
|
||||||
|
// duration as a formatted string.
|
||||||
|
func FormatDuration(d cv.Duration) string {
|
||||||
|
var start string
|
||||||
|
|
||||||
|
startDate, err := d.Start.ParseDate()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
start = "Unknown"
|
||||||
|
} else {
|
||||||
|
start = fmt.Sprintf("%s, %d", startDate.Month().String(), startDate.Year())
|
||||||
|
}
|
||||||
|
|
||||||
|
var end string
|
||||||
|
|
||||||
|
if d.Present {
|
||||||
|
end = "Present"
|
||||||
|
} else {
|
||||||
|
endDate, err := d.End.ParseDate()
|
||||||
|
if err != nil {
|
||||||
|
end = "Unknown"
|
||||||
|
} else {
|
||||||
|
end = fmt.Sprintf("%s, %d", endDate.Month().String(), endDate.Year())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return start + " - " + end
|
||||||
|
}
|
11
internal/templateFuncs/join.go
Normal file
11
internal/templateFuncs/join.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JoinSentences uses strings.Join to join all string elements into
|
||||||
|
// a single string.
|
||||||
|
func JoinSentences(sentences []string) string {
|
||||||
|
return strings.Join(sentences, " ")
|
||||||
|
}
|
7
internal/templateFuncs/notlastelement.go
Normal file
7
internal/templateFuncs/notlastelement.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
// NotLastElement returns true if an element of an array
|
||||||
|
// or a slice is not the last.
|
||||||
|
func NotLastElement(pos, length int) bool {
|
||||||
|
return pos < length-1
|
||||||
|
}
|
23
internal/templateFuncs/withindaterange.go
Normal file
23
internal/templateFuncs/withindaterange.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WithinTimePeriod returns true if the employment's end date is within
|
||||||
|
// the specified time period.
|
||||||
|
func WithinTimePeriod(t time.Time) func(d cv.Duration) bool {
|
||||||
|
return func(d cv.Duration) bool {
|
||||||
|
if d.Present {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := d.After(t)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
47
main.go
47
main.go
|
@ -2,15 +2,18 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
"io"
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/spruce/internal/cv"
|
||||||
|
tf "codeflow.dananglin.me.uk/apollo/spruce/internal/templateFuncs"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/tex/*
|
//go:embed templates/tex/*
|
||||||
|
@ -18,14 +21,24 @@ var templates embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var input, output string
|
var input, output string
|
||||||
|
var employmentHistory int
|
||||||
|
|
||||||
flag.StringVar(&input, "input", "", "specify the CV JSON file that you want to input to the builder.")
|
flag.StringVar(&input, "input", "", "specify the CV JSON file that you want to input to the builder.")
|
||||||
flag.StringVar(&output, "output", "", "specify the name of the output CV file.")
|
flag.StringVar(&output, "output", "", "specify the name of the output CV file.")
|
||||||
|
flag.IntVar(&employmentHistory, "employment-history", 10, "show employment history within these number of years.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := run(input, output, employmentHistory); err != nil {
|
||||||
|
log.Fatalf("ERROR: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(input, output string, employmentHistory int) error {
|
||||||
|
historyLimit := time.Now().AddDate(-1*employmentHistory, 0, 0)
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
|
tempDir, err := os.MkdirTemp("/tmp", "cv-builder-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("ERROR: Unable to create a temporary directory; %v", err)
|
return fmt.Errorf("unable to create a temporary directory; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -35,26 +48,23 @@ func main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
texFile, err := tex(input, tempDir)
|
texFile, err := tex(input, tempDir, historyLimit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("ERROR: %v", err)
|
return fmt.Errorf("unable to create the tex file; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pdf(tempDir, texFile, output); err != nil {
|
if err := pdf(tempDir, texFile, output); err != nil {
|
||||||
log.Fatalf("ERROR: %v", err)
|
return fmt.Errorf("unable to create the PDF file; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tex generates the CV document as a Tex file.
|
// tex generates the CV document as a Tex file.
|
||||||
func tex(input, tempDir string) (string, error) {
|
func tex(input, tempDir string, historyLimit time.Time) (string, error) {
|
||||||
data, err := os.ReadFile(input)
|
c, err := cv.NewCV(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to read data from file; %w", err)
|
return "", fmt.Errorf("unable to create a new CV value from %s; %w", input, err)
|
||||||
}
|
|
||||||
|
|
||||||
var cv Cv
|
|
||||||
if err = json.Unmarshal(data, &cv); err != nil {
|
|
||||||
return "", fmt.Errorf("unable to decode JSON data; %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output := filepath.Join(tempDir, "cv.tex")
|
output := filepath.Join(tempDir, "cv.tex")
|
||||||
|
@ -66,9 +76,10 @@ func tex(input, tempDir string) (string, error) {
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
fmap := template.FuncMap{
|
fmap := template.FuncMap{
|
||||||
"notLastElement": notLastElement,
|
"notLastElement": tf.NotLastElement,
|
||||||
"join": join,
|
"join": tf.JoinSentences,
|
||||||
"durationToString": durationToString,
|
"durationToString": tf.FormatDuration,
|
||||||
|
"withinTimePeriod": tf.WithinTimePeriod(historyLimit),
|
||||||
}
|
}
|
||||||
|
|
||||||
t := template.Must(template.New("cv.tmpl.tex").
|
t := template.Must(template.New("cv.tmpl.tex").
|
||||||
|
@ -77,7 +88,7 @@ func tex(input, tempDir string) (string, error) {
|
||||||
ParseFS(templates, "templates/tex/*.tmpl.tex"),
|
ParseFS(templates, "templates/tex/*.tmpl.tex"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = t.Execute(file, cv); err != nil {
|
if err = t.Execute(file, c); err != nil {
|
||||||
return "", fmt.Errorf("unable to execute the CV template. %w", err)
|
return "", fmt.Errorf("unable to execute the CV template. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// notLastElement returns true if an element of an array
|
|
||||||
// or a slice is not the last.
|
|
||||||
func notLastElement(pos, length int) bool {
|
|
||||||
return pos < length-1
|
|
||||||
}
|
|
||||||
|
|
||||||
// join uses strings.Join to join all string elements into
|
|
||||||
// a single string.
|
|
||||||
func join(s []string) string {
|
|
||||||
return strings.Join(s, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// durationToString outputs the employment/education
|
|
||||||
// duration as a formatted string.
|
|
||||||
func durationToString(d Duration) string {
|
|
||||||
start := fmt.Sprintf("%s, %s", d.Start.Month, d.Start.Year)
|
|
||||||
present := strings.ToLower(d.Present)
|
|
||||||
end := ""
|
|
||||||
|
|
||||||
if present == "yes" || present == "true" {
|
|
||||||
end = "Present"
|
|
||||||
} else {
|
|
||||||
end = fmt.Sprintf("%s, %s", d.End.Month, d.End.Year)
|
|
||||||
}
|
|
||||||
|
|
||||||
return start + " - " + end
|
|
||||||
}
|
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
\section{EXPERIENCE}
|
\section{EXPERIENCE}
|
||||||
<<- range .Employment>>
|
<<- range .Employment>>
|
||||||
|
<<- if withinTimePeriod .Duration>>
|
||||||
\jobsection{<<.Company>>}{<<.Location>>}{<<.JobTitle>>}{<<durationToString .Duration>>}
|
\jobsection{<<.Company>>}{<<.Location>>}{<<.JobTitle>>}{<<durationToString .Duration>>}
|
||||||
\startitemize
|
\startitemize
|
||||||
<<range .Details>>
|
<<range .Details>>
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
<<end>>
|
<<end>>
|
||||||
\stopitemize
|
\stopitemize
|
||||||
<<end>>
|
<<end>>
|
||||||
|
<<end>>
|
||||||
|
|
||||||
\section{EDUCATION}
|
\section{EDUCATION}
|
||||||
<<range .Education ->>
|
<<range .Education ->>
|
||||||
|
|
Loading…
Reference in a new issue