diff --git a/README.md b/README.md new file mode 100644 index 00000000..1e46c457 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# certificates-svc + +## Install + + ``` + git clone https://github.com/Digital-Certificates-DL/certificates-svc + cd certificates-svc + go build main.go + export KV_VIPER_FILE=./config.yaml + ./main migrate up + ./main run service + ``` + +## Documentation + +We do use openapi:json standard for API. We use swagger for documenting our API. + +To open online documentation, go to [swagger editor](http://localhost:8080/swagger-editor/) here is how you can start it +``` + cd docs + npm install + npm start +``` +To build documentation use `npm run build` command, +that will create open-api documentation in `web_deploy` folder. + +To generate resources for Go models run `./generate.sh` script in root folder. +use `./generate.sh --help` to see all available options. + + + +## Running from Source + +* Set up environment value with config file path `KV_VIPER_FILE=./config.yaml` +* Provide valid config file +* Launch the service with `migrate up` command to create database schema +* Launch the service with `run service` command + + +### Database +For services, we do use ***PostgresSQL*** database. +You can [install it locally](https://www.postgresql.org/download/) or use [docker image](https://hub.docker.com/_/postgres/). + + diff --git a/config.yaml b/config.yaml index a64d88a1..04b7061b 100644 --- a/config.yaml +++ b/config.yaml @@ -18,7 +18,7 @@ networks: ipfs_pr_key: "" sbt: - external_url: "https://dlt-academy.com/certificates" + external_url: "https://distributed.education/certificates" google: secret_path: "./client.json" @@ -35,7 +35,7 @@ tables: qr_code: qr_path: "./qr/" - template: "message:\n%s\n\naddress:\n%s\n\nsignature:\n%s\n\ncertificate page:\nhttps://dlt-academy.com/certificates" + template: "message:\n%s\n\naddress:\n%s\n\nsignature:\n%s\n\ncertificate page:\nhttps://distributed.education/certificates" templates: list: diff --git a/docs/spec/components/schemas/ExpiredTokenError.yaml b/docs/spec/components/schemas/ExpiredTokenError.yaml new file mode 100644 index 00000000..f5300d92 --- /dev/null +++ b/docs/spec/components/schemas/ExpiredTokenError.yaml @@ -0,0 +1,14 @@ +allOf: + - $ref: '#/components/schemas/ExpiredTokenKey' + - type: object + required: + - attributes + properties: + attributes: + type: object + required: + - error + properties: + error: + type: boolean + example: false \ No newline at end of file diff --git a/docs/spec/components/schemas/ExpiredTokenKey.yaml b/docs/spec/components/schemas/ExpiredTokenKey.yaml new file mode 100644 index 00000000..4ecf31be --- /dev/null +++ b/docs/spec/components/schemas/ExpiredTokenKey.yaml @@ -0,0 +1,9 @@ +type: object +required: + - type + - id +properties: + type: + type: string + enum: + - expired_token \ No newline at end of file diff --git a/docs/spec/components/schemas/Template.yaml b/docs/spec/components/schemas/Template.yaml index c141b934..d270938c 100644 --- a/docs/spec/components/schemas/Template.yaml +++ b/docs/spec/components/schemas/Template.yaml @@ -11,7 +11,8 @@ allOf: - user properties: user: - type: string + type: object + $ref: '#/components/schemas/UserKey' attributes: type: object required: @@ -19,6 +20,7 @@ allOf: - background_img - is_completed - template_name + - template_short_name properties: template: type: object @@ -29,4 +31,6 @@ allOf: type: boolean template_name: type: string + template_short_name: + type: string diff --git a/internal/assets/migrations/001_initial.sql b/internal/assets/migrations/001_initial.sql index 9b4bb962..0025feb8 100644 --- a/internal/assets/migrations/001_initial.sql +++ b/internal/assets/migrations/001_initial.sql @@ -11,7 +11,8 @@ CREATE TABLE template ( user_id INTEGER REFERENCES users(id), template TEXT, img_bytes text, - name TEXT + name TEXT, + short_name TEXT ); -- +migrate Down diff --git a/internal/data/pg/template.go b/internal/data/pg/template.go index 0bd3c4a2..efe12c8b 100644 --- a/internal/data/pg/template.go +++ b/internal/data/pg/template.go @@ -104,3 +104,10 @@ func (q *TemplateQ) FilterByName(name string) data.TemplateQ { return q } + +func (q *TemplateQ) FilterByShortName(name string) data.TemplateQ { + q.sql = q.sql.Where(sq.Eq{nameField: name}) + q.upd = q.upd.Where(sq.Eq{nameField: name}) + + return q +} diff --git a/internal/data/template.go b/internal/data/template.go index 2b12a8d1..131f3469 100644 --- a/internal/data/template.go +++ b/internal/data/template.go @@ -8,13 +8,15 @@ type TemplateQ interface { Select() ([]Template, error) FilterByUser(ids int64) TemplateQ FilterByName(name string) TemplateQ + FilterByShortName(name string) TemplateQ FilterByID(ids int64) TemplateQ } type Template struct { - ID int64 `db:"id" structs:"-"` - UserID int64 `db:"user_id" structs:"user_id"` - Name string `db:"name" structs:"name"` - Template []byte `db:"template" structs:"template"` - ImgBytes []byte `db:"img_bytes" structs:"img_bytes"` //todo make better + ID int64 `db:"id" structs:"-"` + UserID int64 `db:"user_id" structs:"user_id"` + Name string `db:"name" structs:"name"` + ShortName string `db:"short_name" structs:"short_name"` + Template []byte `db:"template" structs:"template"` + ImgBytes []byte `db:"img_bytes" structs:"img_bytes"` //todo make better } diff --git a/internal/service/api/handlers/check_container_state.go b/internal/service/api/handlers/check_container_state.go index 403821af..2a7c7051 100644 --- a/internal/service/api/handlers/check_container_state.go +++ b/internal/service/api/handlers/check_container_state.go @@ -21,7 +21,7 @@ func CheckContainerState(w http.ResponseWriter, r *http.Request) { container := PdfCreator(r).CheckContainerState(containerID) if container == nil { Log(r).WithError(err).Error("user not found") - w.WriteHeader(http.StatusProcessing) + w.WriteHeader(http.StatusNoContent) return } Log(r).Debug("container on handler: ", container) diff --git a/internal/service/api/handlers/create_template.go b/internal/service/api/handlers/create_template.go index d3a62b8f..bd60ca6b 100644 --- a/internal/service/api/handlers/create_template.go +++ b/internal/service/api/handlers/create_template.go @@ -1,26 +1,23 @@ package handlers import ( - "encoding/base64" "encoding/json" "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/ape/problems" "gitlab.com/tokend/course-certificates/ccp/internal/data" "gitlab.com/tokend/course-certificates/ccp/internal/service/api/requests" - "gitlab.com/tokend/course-certificates/ccp/internal/service/core/pdf" - "gitlab.com/tokend/course-certificates/ccp/resources" "net/http" + "strings" ) func CreateTemplate(w http.ResponseWriter, r *http.Request) { - template, backgroundImg, req, err := requests.NewGenerateTemplate(r) + template, _, req, err := requests.NewGenerateTemplate(r) if err != nil { Log(r).WithError(err).Error("failed to generate template") ape.RenderErr(w, problems.BadRequest(err)...) return } - defaultData := pdf.DefaultData - client, err := MasterQ(r).ClientQ().FilterByName(req.Data.Relationships.User).Get() + client, err := MasterQ(r).ClientQ().FilterByName(req.Data.Relationships.User.Data.ID).Get() if err != nil { Log(r).WithError(err).Error("failed to get client") ape.RenderErr(w, problems.InternalError()) @@ -32,35 +29,6 @@ func CreateTemplate(w http.ResponseWriter, r *http.Request) { return } - if template.Width == 0 || template.High == 0 { - tp := pdf.DefaultTemplateTall - _, _, imgBytes, err := tp.Prepare(defaultData, pdf.NewPDFConfig(Config(r)), MasterQ(r), backgroundImg, client.ID, StaticConfiger(r).Location) - if err != nil { - Log(r).WithError(err).Error("failed to prepare pdf") - ape.RenderErr(w, problems.InternalError()) - return - } - ape.Render(w, newTemplateImageResp(imgBytes)) - return - } - - file := pdf.NewPDF(template.High, template.Width) - - file.SetName(template.Name.X, template.Name.Y, template.Name.FontSize, template.Name.Font) - file.SetDate(template.Date.X, template.Date.Y, template.Date.FontSize, template.Date.Font) - file.SetCourse(template.Course.X, template.Course.Y, template.Course.FontSize, template.Course.Font) - file.SetCredits(template.Credits.X, template.Credits.Y, template.Credits.FontSize, template.Credits.Font) - file.SetExam(template.Exam.X, template.Exam.Y, template.Exam.FontSize, template.Exam.Font) - file.SetLevel(template.Level.X, template.Level.Y, template.Level.FontSize, template.Level.Font) - file.SetSerialNumber(template.SerialNumber.X, template.SerialNumber.Y, template.SerialNumber.FontSize, template.SerialNumber.Font) - file.SetPoints(template.Points.X, template.Points.Y, template.Points.FontSize, template.Points.Font) - file.SetQR(template.QR.X, template.QR.Y, template.QR.FontSize, template.QR.High, template.Width) - _, _, imgBytes, err := template.Prepare(defaultData, pdf.NewPDFConfig(Config(r)), MasterQ(r), backgroundImg, client.ID, StaticConfiger(r).Location) - if err != nil { - Log(r).WithError(err).Error("failed to prepare pdf") - ape.RenderErr(w, problems.InternalError()) - return - } if req.Data.Attributes.IsCompleted { templateBytes, err := json.Marshal(template) if err != nil { @@ -70,10 +38,11 @@ func CreateTemplate(w http.ResponseWriter, r *http.Request) { } err = MasterQ(r).TemplateQ().Insert(&data.Template{ - Template: templateBytes, - //ImgBytes: backgroundImg, - Name: req.Data.Attributes.TemplateName, - UserID: client.ID, + Template: templateBytes, + ImgBytes: []byte(strings.Replace(req.Data.Attributes.BackgroundImg, "data:image/png;base64,", "", 1)), + Name: req.Data.Attributes.TemplateName, + ShortName: req.Data.Attributes.TemplateShortName, + UserID: client.ID, }) if err != nil { Log(r).WithError(err).Error("failed to insert template") @@ -81,16 +50,6 @@ func CreateTemplate(w http.ResponseWriter, r *http.Request) { return } } - ape.Render(w, newTemplateImageResp(imgBytes)) + w.WriteHeader(http.StatusNoContent) return } - -func newTemplateImageResp(img []byte) resources.TemplateResponse { - return resources.TemplateResponse{ - Data: resources.Template{ - Attributes: resources.TemplateAttributes{ - BackgroundImg: base64.StdEncoding.EncodeToString(img), - }, - }, - } -} diff --git a/internal/service/api/handlers/get_templates.go b/internal/service/api/handlers/get_templates.go index b79c6301..e8b5aafd 100644 --- a/internal/service/api/handlers/get_templates.go +++ b/internal/service/api/handlers/get_templates.go @@ -41,9 +41,9 @@ func GetTemplates(w http.ResponseWriter, r *http.Request) { } func newTemlateListResp(tmps []data.Template) resources.TemplateListResponse { - var reponseTmpList []resources.Template + responseTmpList := make([]resources.Template, 0) for _, tmp := range tmps { - reponseTmpList = append(reponseTmpList, resources.Template{ + responseTmpList = append(responseTmpList, resources.Template{ Attributes: resources.TemplateAttributes{ BackgroundImg: string(tmp.ImgBytes), TemplateName: tmp.Name, @@ -51,6 +51,6 @@ func newTemlateListResp(tmps []data.Template) resources.TemplateListResponse { }) } return resources.TemplateListResponse{ - Data: reponseTmpList, + Data: responseTmpList, } } diff --git a/internal/service/api/handlers/get_users.go b/internal/service/api/handlers/get_users.go index 66b45a36..768f1f8d 100644 --- a/internal/service/api/handlers/get_users.go +++ b/internal/service/api/handlers/get_users.go @@ -61,7 +61,7 @@ func GetUsers(w http.ResponseWriter, r *http.Request) { } Log(r).Error("failed to parse table: Errors:", errs) - ape.RenderErr(w, problems.BadRequest(err)...) + ape.RenderErr(w, problems.Unauthorized()) return } diff --git a/internal/service/api/requests/create_template.go b/internal/service/api/requests/create_template.go index 864635e9..5a6a281a 100644 --- a/internal/service/api/requests/create_template.go +++ b/internal/service/api/requests/create_template.go @@ -9,7 +9,7 @@ import ( "gitlab.com/tokend/course-certificates/ccp/internal/service/core/pdf" "gitlab.com/tokend/course-certificates/ccp/resources" "image" - "image/jpeg" + "image/png" "net/http" "strings" ) @@ -52,7 +52,7 @@ func base64toJpg(data string) ([]byte, error) { } buf := new(bytes.Buffer) - if err = jpeg.Encode(buf, m, &jpeg.Options{Quality: 75}); err != nil { + if err = png.Encode(buf, m); err != nil { return nil, err } diff --git a/internal/service/api/requests/ipfs_upload.go b/internal/service/api/requests/ipfs_upload.go index dbaf6796..da0467af 100644 --- a/internal/service/api/requests/ipfs_upload.go +++ b/internal/service/api/requests/ipfs_upload.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" "gitlab.com/tokend/course-certificates/ccp/resources" "net/http" - "regexp" ) type IpfsFileUpload struct { @@ -32,7 +31,5 @@ func validateIpfsData(request resources.IpfsFileUpload) error { validation.Required), "/attributes/img": validation.Validate(request.Attributes.Img, validation.Required), - "/attributes/name": validation.Validate(request.Attributes.Name, - validation.Required, validation.Match(regexp.MustCompile("^([A-Za-z])[A-Za-z\\s]+$"))), }).Filter() } diff --git a/internal/service/api/router.go b/internal/service/api/router.go index a43b7717..82680102 100644 --- a/internal/service/api/router.go +++ b/internal/service/api/router.go @@ -36,6 +36,7 @@ func (s *service) router(cfg config.Config) chi.Router { r.Route("/certificate", func(r chi.Router) { r.Post("/", handlers.PrepareCertificate) r.Post("/bitcoin", handlers.UpdateCertificate) + r.Put("/", handlers.UpdateCertificate) r.Post("/ipfs", handlers.UploadFileToIpfs) r.Get("/{container}", handlers.CheckContainerState) diff --git a/internal/service/core/google/google_drive.go b/internal/service/core/google/google_drive.go index e14e1fe9..75295d15 100644 --- a/internal/service/core/google/google_drive.go +++ b/internal/service/core/google/google_drive.go @@ -47,11 +47,11 @@ func (g *Google) GetFiles() ([]*drive.File, error) { if len(r.Files) == 0 { return nil, errors.New("No files found.") - } else { - for _, i := range r.Files { - g.cfg.Log().Info("%s (%s)\n", i.Name, i.Id) - } } + for _, i := range r.Files { + g.cfg.Log().Info("%s (%s)\n", i.Name, i.Id) + } + return r.Files, nil } diff --git a/internal/service/core/pdf/container.go b/internal/service/core/pdf/container.go index 39553f82..10f5238d 100644 --- a/internal/service/core/pdf/container.go +++ b/internal/service/core/pdf/container.go @@ -53,9 +53,14 @@ func (c *Container) Generate() error { files = append(files, google.FilesBytes{File: file, Name: name, ID: user.ID, Type: "image/svg+xml"}) pdf := PDF{} - certificate := pdf.SetTemplateData(DefaultTemplateTall) + certificateTemplate, err := pdf.InitTemplate(c.masterQ, user.CourseTitle, c.owner.ID) + if err != nil { + return errors.Wrap(err, "failed to get template") + } + + certificate := pdf.SetTemplateData(*certificateTemplate) - pdfData := NewData(user.Participant, user.CourseTitle, "45 hours / 1.5 ECTS Credit", user.Points, user.SerialNumber, user.Date, img, user.Note, "", "") + pdfData := NewData(user.Participant, user.CourseTitle, certificateTemplate.Credits.Text, user.Points, user.SerialNumber, user.Date, img, user.Note, "", "") fileBytes, name, certificateImg, err := certificate.Prepare(pdfData, NewPDFConfig(c.config), c.masterQ, nil, c.owner.ID, c.config.StaticConfig().Location) if err != nil { return errors.Wrap(err, "failed to create pdf") @@ -105,9 +110,14 @@ func (c *Container) Update() error { files = append(files, google.FilesBytes{File: file, Name: name, ID: user.ID, Type: "image/svg+xml"}) pdf := PDF{} - certificate := pdf.SetTemplateData(DefaultTemplateTall) + certificateTemplate, err := pdf.InitTemplate(c.masterQ, user.CourseTitle, c.owner.ID) + if err != nil { + return errors.Wrap(err, "failed to get template") + } + + certificate := pdf.SetTemplateData(*certificateTemplate) - pdfData := NewData(user.Participant, user.CourseTitle, "45 hours / 1.5 ECTS Credit", user.Points, user.SerialNumber, user.Date, img, user.Note, "", "") + pdfData := NewData(user.Participant, user.CourseTitle, certificateTemplate.Credits.Text, user.Points, user.SerialNumber, user.Date, img, user.Note, "", "") fileBytes, name, certificateImg, err := certificate.Prepare(pdfData, NewPDFConfig(c.config), c.masterQ, nil, c.owner.ID, c.config.StaticConfig().Location) if err != nil { return errors.Wrap(err, "failed to create pdf") diff --git a/internal/service/core/pdf/default_data.go b/internal/service/core/pdf/default_data.go index 6891f23e..fc53c22f 100644 --- a/internal/service/core/pdf/default_data.go +++ b/internal/service/core/pdf/default_data.go @@ -1,8 +1,8 @@ package pdf var DefaultTemplateNormal = PDF{ - High: 595, - Width: 842, + Height: 595, + Width: 842, Name: Field{ X: 200, Y: 217, @@ -40,10 +40,10 @@ var DefaultTemplateNormal = PDF{ Font: "regular", }, QR: Field{ - X: 658, - Y: 106, - High: 114, - Width: 114, + X: 658, + Y: 106, + Height: 114, + Width: 114, }, Exam: Field{ X: 300, @@ -60,8 +60,8 @@ var DefaultTemplateNormal = PDF{ } var DefaultTemplateTall = PDF{ - High: 1190, - Width: 1684, + Height: 1190, + Width: 1684, Name: Field{ Y: 434, FontSize: 56, @@ -77,6 +77,7 @@ var DefaultTemplateTall = PDF{ Y: 112, FontSize: 24, Font: "regular", + Text: "45 hours / 1.5 ECTS Credit", }, Points: Field{ X: 140, @@ -99,10 +100,10 @@ var DefaultTemplateTall = PDF{ Font: "regular", }, QR: Field{ - X: 1316, - Y: 212, - High: 228, - Width: 228, + X: 1316, + Y: 212, + Height: 228, + Width: 228, }, Exam: Field{ Y: 600, diff --git a/internal/service/core/pdf/pdf.go b/internal/service/core/pdf/pdf.go index c145b258..c3718f3f 100644 --- a/internal/service/core/pdf/pdf.go +++ b/internal/service/core/pdf/pdf.go @@ -2,6 +2,7 @@ package pdf import ( "bytes" + "encoding/json" "fmt" "github.com/signintech/gopdf" "gitlab.com/distributed_lab/logan/v3/errors" @@ -9,12 +10,13 @@ import ( "image" "io" "os" + "strconv" "strings" ) func (p *PDF) Prepare(data PDFData, config *PDFConfig, masterQ data.MasterQ, backgroundImg []byte, userID int64, abs string) ([]byte, string, []byte, error) { pdf := new(gopdf.GoPdf) - pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: p.Width, H: p.High}}) + pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: p.Width, H: p.Height}}) pdf.AddPage() pdf.SetTextColor(255, 255, 255) @@ -24,6 +26,9 @@ func (p *PDF) Prepare(data PDFData, config *PDFConfig, masterQ data.MasterQ, bac templateImg := config.templates[data.Course] + if templateImg == "" { + templateImg = data.Course + } if backgroundImg == nil { if err := p.initBackground(pdf, masterQ.TemplateQ(), templateImg, abs, userID); err != nil { return nil, "", nil, errors.Wrap(err, "failed to init background") @@ -94,7 +99,7 @@ func (p *PDF) setBackground(pdf *gopdf.GoPdf, image []byte) error { return errors.Wrap(err, "failed to prepare background") } - err = pdf.ImageByHolder(backgroundImgHolder, 0, 0, &gopdf.Rect{W: p.Width, H: p.High}) + err = pdf.ImageByHolder(backgroundImgHolder, 0, 0, &gopdf.Rect{W: p.Width, H: p.Height}) if err != nil { return errors.Wrap(err, "failed to set background") } @@ -119,9 +124,28 @@ func (p *PDF) setLevel(pdf *gopdf.GoPdf, level string) error { if err := pdf.SetFont("italic", "", p.Level.FontSize); err != nil { return errors.Wrap(err, "failed to set font Level") } - pdf.SetX(0) pdf.SetY(p.Level.Y) - if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.High}, level, gopdf.CellOption{Align: gopdf.Center}); err != nil { + pdf.SetTextColor(255, 255, 255) + + if p.Level.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Level.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.Level.XCenter { + pdf.SetX(0) + + if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.Height}, level, gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell Level") + } + + return nil + } + + pdf.SetX(p.Level.X) + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, level); err != nil { return errors.Wrap(err, "failed to cell Level") } @@ -132,10 +156,27 @@ func (p *PDF) setExam(pdf *gopdf.GoPdf, exam string) error { if err := pdf.SetFont("italic", "", p.Exam.FontSize); err != nil { return errors.Wrap(err, "failed to set font Exam") } - pdf.SetX(0) pdf.SetY(p.Exam.Y) + pdf.SetTextColor(255, 255, 255) - if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.High}, exam, gopdf.CellOption{Align: gopdf.Center}); err != nil { + if p.Exam.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Exam.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.Exam.XCenter { + pdf.SetX(0) + if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.Height}, exam, gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell Exam") + } + + return nil + } + + pdf.SetX(p.Exam.X) + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, exam); err != nil { return errors.Wrap(err, "failed to cell Exam") } @@ -148,7 +189,7 @@ func (p *PDF) setQR(pdf *gopdf.GoPdf, qr []byte) error { return errors.Wrap(err, "failed to convert bytes to image QR") } - err = pdf.ImageFrom(img, p.QR.X, p.QR.Y, &gopdf.Rect{W: p.QR.High, H: p.QR.High}) + err = pdf.ImageFrom(img, p.QR.X, p.QR.Y, &gopdf.Rect{W: p.QR.Width, H: p.QR.Height}) if err != nil { return errors.Wrap(err, "failed to set image QR") } @@ -156,14 +197,35 @@ func (p *PDF) setQR(pdf *gopdf.GoPdf, qr []byte) error { return nil } -func (p *PDF) setCourse(pdf *gopdf.GoPdf, courseTitle string) error { +func (p *PDF) setCourse(pdf *gopdf.GoPdf, courseTitle string, templateImg string) error { if err := pdf.SetFont("italic", "", p.Course.FontSize); err != nil { return errors.Wrap(err, "failed to set font Course") } - pdf.SetX(0) + pdf.SetY(p.Course.Y) + pdf.SetTextColor(255, 255, 255) - if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.High}, courseTitle, gopdf.CellOption{Align: gopdf.Center}); err != nil { + if p.Course.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Course.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if courseTitle == "" { + courseTitle = templateImg + } + + if p.Course.XCenter { + pdf.SetX(0) + if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.Height}, courseTitle, gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell Course") + } + return nil + } + + pdf.SetX(p.Course.X) + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, courseTitle); err != nil { return errors.Wrap(err, "failed to cell Course") } @@ -175,8 +237,27 @@ func (p *PDF) setSerialNumber(pdf *gopdf.GoPdf, serialNumber string) error { return errors.Wrap(err, "failed to set font SerialNumber") } - pdf.SetX(p.SerialNumber.X) pdf.SetY(p.SerialNumber.Y) + pdf.SetTextColor(255, 255, 255) + + if p.SerialNumber.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.SerialNumber.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.SerialNumber.XCenter { + pdf.SetX(0) + + if err := pdf.CellWithOption(&gopdf.Rect{W: 300, H: 300}, serialNumber, gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell SerialNumber ") + } + return nil + } + + pdf.SetX(p.SerialNumber.X) + if err := pdf.CellWithOption(&gopdf.Rect{W: 300, H: 300}, serialNumber, gopdf.CellOption{Align: gopdf.Right}); err != nil { return errors.Wrap(err, "failed to cell SerialNumber ") } @@ -190,9 +271,28 @@ func (p *PDF) setPoints(pdf *gopdf.GoPdf, points string) error { return errors.Wrap(err, "failed to set font points") } - pdf.SetX(p.Points.X) pdf.SetY(p.Points.Y) - if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.High}, fmt.Sprintf("Count of points: %s", points)); err != nil { + pdf.SetTextColor(255, 255, 255) + + if p.Points.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Points.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.Points.XCenter { + pdf.SetX(0) + + if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.Height}, fmt.Sprintf("Count of points: %s", points), gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell points") + } + + return nil + } + + pdf.SetX(p.Points.X) + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, fmt.Sprintf("Count of points: %s", points)); err != nil { return errors.Wrap(err, "failed to cell points") } @@ -204,8 +304,26 @@ func (p *PDF) setDate(pdf *gopdf.GoPdf, date string) error { return errors.Wrap(err, "failed to set font Date") } - pdf.SetX(p.Date.X) pdf.SetY(p.Date.Y) + pdf.SetTextColor(255, 255, 255) + + if p.Date.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Date.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.Date.XCenter { + pdf.SetX(0) + + if err := pdf.CellWithOption(&gopdf.Rect{W: 300, H: 300}, fmt.Sprintf("Issued on: %s", date), gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell Date") + } + return nil + } + pdf.SetX(p.Date.X) + if err := pdf.CellWithOption(&gopdf.Rect{W: 300, H: 300}, fmt.Sprintf("Issued on: %s", date), gopdf.CellOption{Align: gopdf.Right}); err != nil { return errors.Wrap(err, "failed to cell Date") } @@ -218,8 +336,16 @@ func (p *PDF) setCredits(pdf *gopdf.GoPdf, credits string) error { } pdf.SetX(p.Credits.X) pdf.SetY(p.Credits.Y) + pdf.SetTextColor(255, 255, 255) + + if p.Credits.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Credits.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } - if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.High}, fmt.Sprintf(credits)); err != nil { + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, fmt.Sprintf(credits)); err != nil { return errors.Wrap(err, "failed to cell credits") } @@ -231,12 +357,31 @@ func (p *PDF) setName(pdf *gopdf.GoPdf, name string) error { return errors.Wrap(err, "failed to set font name") } pdf.SetY(p.Name.Y) - pdf.SetX(0) - if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.High}, name, gopdf.CellOption{Align: gopdf.Center}); err != nil { + pdf.SetTextColor(255, 255, 255) + + if p.Name.Color != "" { + rgb, err := p.hex2RGB(strings.Replace(p.Name.Color, "#", "", 1)) + if err == nil { + pdf.SetTextColor(rgb.Red, rgb.Green, rgb.Blue) + } + } + + if p.Name.XCenter { + pdf.SetX(0) + + if err := pdf.CellWithOption(&gopdf.Rect{W: p.Width, H: p.Height}, name, gopdf.CellOption{Align: gopdf.Center}); err != nil { + return errors.Wrap(err, "failed to cell name") + } + return nil + } + + pdf.SetX(p.Name.X) + if err := pdf.Cell(&gopdf.Rect{W: p.Width, H: p.Height}, name); err != nil { return errors.Wrap(err, "failed to cell name") } return nil + } func (p *PDF) initBackground(pdf *gopdf.GoPdf, templateQ data.TemplateQ, templateImg, abs string, userID int64) error { @@ -268,20 +413,48 @@ func (p *PDF) prepareName(name, course string) string { } func (p *PDF) SetTemplateData(template PDF) *PDF { - certificate := NewPDF(template.High, template.Width) - certificate.SetName(template.Name.X, template.Name.Y, template.Name.FontSize, template.Name.Font) - certificate.SetDate(template.Date.X, template.Date.Y, template.Date.FontSize, template.Date.Font) - certificate.SetCourse(template.Course.X, template.Course.Y, template.Course.FontSize, template.Course.Font) - certificate.SetCredits(template.Credits.X, template.Credits.Y, template.Credits.FontSize, template.Credits.Font) - certificate.SetExam(template.Exam.X, template.Exam.Y, template.Exam.FontSize, template.Exam.Font) - certificate.SetLevel(template.Level.X, template.Level.Y, template.Level.FontSize, template.Level.Font) - certificate.SetSerialNumber(template.SerialNumber.X, template.SerialNumber.Y, template.SerialNumber.FontSize, template.SerialNumber.Font) - certificate.SetPoints(template.Points.X, template.Points.Y, template.Points.FontSize, template.Points.Font) - certificate.SetQR(template.QR.X, template.QR.Y, template.QR.FontSize, template.QR.High, template.Width) + certificate := NewPDF(template.Height, template.Width) + certificate.SetName(template.Name.X, template.Name.Y, template.Name.FontSize, template.Name.Font, template.Name.Color, template.Name.XCenter) + certificate.SetDate(template.Date.X, template.Date.Y, template.Date.FontSize, template.Date.Font, template.Date.Color, template.Date.XCenter) + certificate.SetCourse(template.Course.X, template.Course.Y, template.Course.FontSize, template.Course.Font, template.Course.Color, template.Course.Text, template.Course.XCenter) + certificate.SetCredits(template.Credits.X, template.Credits.Y, template.Credits.FontSize, template.Credits.Font, template.Credits.Color, template.Credits.XCenter) + certificate.SetExam(template.Exam.X, template.Exam.Y, template.Exam.FontSize, template.Exam.Font, template.Exam.Color, template.Exam.XCenter) + certificate.SetLevel(template.Level.X, template.Level.Y, template.Level.FontSize, template.Level.Font, template.Level.Color, template.Level.Text, template.Level.XCenter) + certificate.SetSerialNumber(template.SerialNumber.X, template.SerialNumber.Y, template.SerialNumber.FontSize, template.SerialNumber.Font, template.SerialNumber.Color, template.SerialNumber.XCenter) + certificate.SetPoints(template.Points.X, template.Points.Y, template.Points.FontSize, template.Points.Font, template.Points.Color, template.Points.XCenter) + certificate.SetQR(template.QR.X, template.QR.Y, template.QR.FontSize, template.QR.Height, template.QR.Width) return certificate } +func (p *PDF) InitTemplate(masterQ data.MasterQ, templateName string, userID int64) (*PDF, error) { + template, err := masterQ.TemplateQ().FilterByName(templateName).FilterByUser(userID).Get() + if err != nil { + return nil, errors.Wrap(err, "failed to get template data") + } + if template == nil || template.Template == nil { + return &DefaultTemplateTall, nil + } + + pdf, err := p.templateDecoder(template.Template) + if err != nil { + return nil, errors.Wrap(err, "failed to decode template") + } + + return pdf, nil + +} + +func (p *PDF) templateDecoder(templateBytes []byte) (*PDF, error) { + pdf := new(PDF) + err := json.Unmarshal(templateBytes, pdf) + if err != nil { + return nil, errors.Wrap(err, "failed to decode template") + } + + return pdf, nil +} + func (p *PDF) CellAllPdfFields(pdf *gopdf.GoPdf, data PDFData, config *PDFConfig, templateImg string) error { if err := p.setName(pdf, data.Name); err != nil { return errors.Wrap(err, "failed to set name") @@ -304,7 +477,13 @@ func (p *PDF) CellAllPdfFields(pdf *gopdf.GoPdf, data PDFData, config *PDFConfig } isLevel, title, level := p.checkLevel(config.titles[templateImg]) - if err := p.setCourse(pdf, title); err != nil { + if title == "" { + level = p.Level.Text + title = p.Course.Text + isLevel = len(level) > 0 + } + + if err := p.setCourse(pdf, title, templateImg); err != nil { return errors.Wrap(err, "failed to set course") } @@ -326,3 +505,26 @@ func (p *PDF) CellAllPdfFields(pdf *gopdf.GoPdf, data PDFData, config *PDFConfig return nil } + +type RGB struct { + Red uint8 + Green uint8 + Blue uint8 +} + +func (p *PDF) hex2RGB(hex string) (RGB, error) { + var rgb RGB + values, err := strconv.ParseUint(hex, 16, 32) + + if err != nil { + return RGB{}, err + } + + rgb = RGB{ + Red: uint8(values >> 16), + Green: uint8((values >> 8) & 0xFF), + Blue: uint8(values & 0xFF), + } + + return rgb, nil +} diff --git a/internal/service/core/pdf/setters.go b/internal/service/core/pdf/setters.go index 2273c4a2..5c64c978 100644 --- a/internal/service/core/pdf/setters.go +++ b/internal/service/core/pdf/setters.go @@ -2,8 +2,8 @@ package pdf func NewPDF(high, width float64) *PDF { return &PDF{ - High: high, - Width: width, + Height: high, + Width: width, } } @@ -22,77 +22,93 @@ func NewData(name, course, credits, points, serialNumber, date string, qr []byte } } -func (p *PDF) SetName(x, y float64, size int, font string) { +func (p *PDF) SetName(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.Name = fl } -func (p *PDF) SetCourse(x, y float64, size int, font string) { +func (p *PDF) SetCourse(x, y float64, size int, font string, color string, text string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + Text: text, + XCenter: isXCenter, } p.Course = fl } -func (p *PDF) SetCredits(x, y float64, size int, font string) { +func (p *PDF) SetCredits(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.Credits = fl } -func (p *PDF) SetLevel(x, y float64, size int, font string) { +func (p *PDF) SetLevel(x, y float64, size int, font string, color string, text string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + Text: text, + XCenter: isXCenter, } p.Level = fl } -func (p *PDF) SetPoints(x, y float64, size int, font string) { +func (p *PDF) SetPoints(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.Points = fl } -func (p *PDF) SetSerialNumber(x, y float64, size int, font string) { +func (p *PDF) SetSerialNumber(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.SerialNumber = fl } -func (p *PDF) SetDate(x, y float64, size int, font string) { +func (p *PDF) SetDate(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.Date = fl @@ -102,19 +118,21 @@ func (p *PDF) SetQR(x, y float64, size int, high, width float64) { X: x, Y: y, FontSize: size, - High: high, + Height: high, Width: width, } p.QR = fl } -func (p *PDF) SetExam(x, y float64, size int, font string) { +func (p *PDF) SetExam(x, y float64, size int, font string, color string, isXCenter bool) { fl := Field{ X: x, Y: y, FontSize: size, Font: font, + Color: color, + XCenter: isXCenter, } p.Exam = fl diff --git a/internal/service/core/pdf/types.go b/internal/service/core/pdf/types.go index 1d354333..b391a045 100644 --- a/internal/service/core/pdf/types.go +++ b/internal/service/core/pdf/types.go @@ -6,7 +6,7 @@ const ( ) type PDF struct { - High float64 `json:"high"` + Height float64 `json:"height"` Width float64 `json:"width"` Name Field `json:"name"` Course Field `json:"course"` @@ -28,8 +28,9 @@ type Field struct { FontSize int `json:"font_size"` Color string `json:"color"` Font string `json:"font"` - High float64 `json:"high"` + Height float64 `json:"height"` Width float64 `json:"width"` + Text string `json:"text"` } type PDFData struct { diff --git a/resources/model_expired_token_error.go b/resources/model_expired_token_error.go new file mode 100644 index 00000000..c7cf7a95 --- /dev/null +++ b/resources/model_expired_token_error.go @@ -0,0 +1,31 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type ExpiredTokenError struct { + Key + Attributes ExpiredTokenErrorAttributes `json:"attributes"` +} +type ExpiredTokenErrorResponse struct { + Data ExpiredTokenError `json:"data"` + Included Included `json:"included"` +} + +type ExpiredTokenErrorListResponse struct { + Data []ExpiredTokenError `json:"data"` + Included Included `json:"included"` + Links *Links `json:"links"` +} + +// MustExpiredTokenError - returns ExpiredTokenError from include collection. +// if entry with specified key does not exist - returns nil +// if entry with specified key exists but type or ID mismatches - panics +func (c *Included) MustExpiredTokenError(key Key) *ExpiredTokenError { + var expiredTokenError ExpiredTokenError + if c.tryFindEntry(key, &expiredTokenError) { + return &expiredTokenError + } + return nil +} diff --git a/resources/model_expired_token_error_attributes.go b/resources/model_expired_token_error_attributes.go new file mode 100644 index 00000000..23233b3c --- /dev/null +++ b/resources/model_expired_token_error_attributes.go @@ -0,0 +1,9 @@ +/* + * GENERATED. Do not modify. Your changes might be overwritten! + */ + +package resources + +type ExpiredTokenErrorAttributes struct { + Error bool `json:"error"` +} diff --git a/resources/model_resource_type.go b/resources/model_resource_type.go index 8f4bb417..13948a59 100644 --- a/resources/model_resource_type.go +++ b/resources/model_resource_type.go @@ -9,6 +9,7 @@ type ResourceType string // List of ResourceType const ( CONTAINER ResourceType = "container" + EXPIRED_TOKEN ResourceType = "expired_token" IPFS_FILE ResourceType = "ipfs_file" IPFS_FILE_UPLOAD ResourceType = "ipfs_file_upload" LINK ResourceType = "link" diff --git a/resources/model_template_attributes.go b/resources/model_template_attributes.go index e175c7f3..e1fdbe33 100644 --- a/resources/model_template_attributes.go +++ b/resources/model_template_attributes.go @@ -7,8 +7,9 @@ package resources import "encoding/json" type TemplateAttributes struct { - BackgroundImg string `json:"background_img"` - IsCompleted bool `json:"is_completed"` - Template json.RawMessage `json:"template"` - TemplateName string `json:"template_name"` + BackgroundImg string `json:"background_img"` + IsCompleted bool `json:"is_completed"` + Template json.RawMessage `json:"template"` + TemplateName string `json:"template_name"` + TemplateShortName string `json:"template_short_name"` } diff --git a/resources/model_template_relationships.go b/resources/model_template_relationships.go index 228cba84..131aa9bf 100644 --- a/resources/model_template_relationships.go +++ b/resources/model_template_relationships.go @@ -5,5 +5,5 @@ package resources type TemplateRelationships struct { - User string `json:"user"` + User Relation `json:"user"` }