generated from Avanade/avanade-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #145 from Avanade/sprint-35
Sprint 35
Showing
41 changed files
with
985 additions
and
787 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
package item | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"main/config" | ||
"main/model" | ||
"main/pkg/session" | ||
"main/service" | ||
"net/http" | ||
|
||
"github.com/gorilla/mux" | ||
) | ||
|
||
type itemPageController struct { | ||
*service.Service | ||
CommunityPortalAppId string | ||
} | ||
|
||
func NewItemPageController(s *service.Service, conf config.ConfigManager) ItemPageController { | ||
return &itemPageController{ | ||
Service: s, | ||
CommunityPortalAppId: conf.GetCommunityPortalAppId(), | ||
} | ||
} | ||
|
||
func (c *itemPageController) MyRequests(w http.ResponseWriter, r *http.Request) { | ||
session, err := session.Store.Get(r, "auth-session") | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
var profile map[string]interface{} | ||
u := session.Values["profile"] | ||
profile, ok := u.(map[string]interface{}) | ||
if !ok { | ||
http.Error(w, "Error getting user data", http.StatusInternalServerError) | ||
return | ||
} | ||
user := model.AzureUser{ | ||
Name: profile["name"].(string), | ||
Email: profile["preferred_username"].(string), | ||
} | ||
|
||
application, err := c.Service.Application.GetApplicationById(c.CommunityPortalAppId) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
b, err := json.Marshal(application) | ||
if err != nil { | ||
fmt.Println(err) | ||
return | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("myrequests", r.URL.Path, user, string(b)) | ||
|
||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} | ||
|
||
func (c *itemPageController) MyApprovals(w http.ResponseWriter, r *http.Request) { | ||
session, err := session.Store.Get(r, "auth-session") | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
var profile map[string]interface{} | ||
u := session.Values["profile"] | ||
profile, ok := u.(map[string]interface{}) | ||
if !ok { | ||
http.Error(w, "Error getting user data", http.StatusInternalServerError) | ||
return | ||
} | ||
user := model.AzureUser{ | ||
Name: profile["name"].(string), | ||
Email: profile["preferred_username"].(string), | ||
} | ||
|
||
application, err := c.Service.Application.GetApplicationById(c.CommunityPortalAppId) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
b, err := json.Marshal(application) | ||
if err != nil { | ||
fmt.Println(err) | ||
return | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("myapprovals", r.URL.Path, user, string(b)) | ||
|
||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} | ||
|
||
func (c *itemPageController) RespondToItem(w http.ResponseWriter, r *http.Request) { | ||
session, _ := session.Store.Get(r, "auth-session") | ||
|
||
var profile map[string]interface{} | ||
u := session.Values["profile"] | ||
profile, ok := u.(map[string]interface{}) | ||
if !ok { | ||
http.Error(w, "Error getting user data", http.StatusInternalServerError) | ||
return | ||
} | ||
user := model.AzureUser{ | ||
Name: profile["name"].(string), | ||
Email: profile["preferred_username"].(string), | ||
} | ||
|
||
params := mux.Vars(r) | ||
|
||
itemIsAuthorized, err := c.Service.Item.ItemIsAuthorized( | ||
params["appGuid"], | ||
params["appModuleGuid"], | ||
params["itemGuid"], | ||
user.Email, | ||
) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
if !itemIsAuthorized.IsAuthorized { | ||
t, d := c.Service.Template.UseTemplate("Unauthorized", r.URL.Path, user, nil) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} else { | ||
if itemIsAuthorized.IsApproved != nil { | ||
var text string | ||
if itemIsAuthorized.IsApproved.Value { | ||
text = "approved" | ||
} else { | ||
text = "rejected" | ||
} | ||
|
||
data := RespondePageData{ | ||
Response: text, | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("already-processed", r.URL.Path, user, data) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} else { | ||
item, err := c.Service.Item.GetItemById(params["itemGuid"]) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
data := RespondePageData{ | ||
ApplicationId: params["appGuid"], | ||
ApplicationModuleId: params["appModuleGuid"], | ||
ItemId: params["itemGuid"], | ||
ApproverEmail: user.Email, | ||
IsApproved: params["isApproved"], | ||
Data: *item, | ||
RequireRemarks: itemIsAuthorized.RequireRemarks, | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("response", r.URL.Path, user, data) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (c *itemPageController) ReassignApproval(w http.ResponseWriter, r *http.Request) { | ||
session, _ := session.Store.Get(r, "auth-session") | ||
|
||
var profile map[string]interface{} | ||
u := session.Values["profile"] | ||
profile, ok := u.(map[string]interface{}) | ||
if !ok { | ||
http.Error(w, "Error getting user data", http.StatusInternalServerError) | ||
return | ||
} | ||
user := model.AzureUser{ | ||
Name: profile["name"].(string), | ||
Email: profile["preferred_username"].(string), | ||
} | ||
|
||
params := mux.Vars(r) | ||
|
||
itemIsAuthorized, err := c.Service.Item.ItemIsAuthorized( | ||
params["appGuid"], | ||
params["appModuleGuid"], | ||
params["itemGuid"], | ||
user.Email, | ||
) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
if !itemIsAuthorized.IsAuthorized { | ||
t, d := c.Service.Template.UseTemplate("Unauthorized", r.URL.Path, user, nil) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} else { | ||
if itemIsAuthorized.IsApproved != nil { | ||
var text string | ||
if itemIsAuthorized.IsApproved.Value { | ||
text = "approved" | ||
} else { | ||
text = "rejected" | ||
} | ||
|
||
data := RespondePageData{ | ||
Response: text, | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("already-processed", r.URL.Path, user, data) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} else { | ||
item, err := c.Service.Item.GetItemById(params["itemGuid"]) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
data := RespondePageData{ | ||
ApplicationId: params["appGuid"], | ||
ApplicationModuleId: params["appModuleGuid"], | ||
ItemId: params["itemGuid"], | ||
Data: *item, | ||
ApproveText: params["ApproveText"], | ||
RejectText: params["RejectText"], | ||
} | ||
|
||
t, d := c.Service.Template.UseTemplate("reassign", r.URL.Path, user, data) | ||
err = t.Execute(w, d) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package model | ||
|
||
type Application struct { | ||
Id string | ||
Name string | ||
ExportUrl string | ||
OrganizationTypeUrl string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
src/goapp/repository/application/application-repository-interface.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package application | ||
|
||
import ( | ||
"main/model" | ||
) | ||
|
||
type ApplicationRepository interface { | ||
GetApplicationById(id string) (*model.Application, error) | ||
} |
44 changes: 44 additions & 0 deletions
44
src/goapp/repository/application/application-repository.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package application | ||
|
||
import ( | ||
"database/sql" | ||
db "main/infrastructure/database" | ||
"main/model" | ||
) | ||
|
||
type applicationRepository struct { | ||
*db.Database | ||
} | ||
|
||
func NewApplicationRepository(db *db.Database) ApplicationRepository { | ||
return &applicationRepository{ | ||
Database: db, | ||
} | ||
} | ||
|
||
func (r *applicationRepository) GetApplicationById(id string) (*model.Application, error) { | ||
var application model.Application | ||
rowApplication, err := r.Query("PR_Applications_Select_ById", sql.Named("Id", id)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
applications, err := r.RowsToMap(rowApplication) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(applications) == 0 { | ||
return nil, nil | ||
} else { | ||
application.Id = applications[0]["Id"].(string) | ||
application.Name = applications[0]["Name"].(string) | ||
if applications[0]["ExportUrl"] != nil { | ||
application.ExportUrl = applications[0]["ExportUrl"].(string) | ||
} | ||
if applications[0]["OrganizationTypeUrl"] != nil { | ||
application.OrganizationTypeUrl = applications[0]["OrganizationTypeUrl"].(string) | ||
} | ||
} | ||
return &application, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
src/goapp/service/application/application-service-interface.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package application | ||
|
||
import ( | ||
"main/model" | ||
) | ||
|
||
type ApplicationService interface { | ||
GetApplicationById(id string) (*model.Application, error) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package application | ||
|
||
import ( | ||
"main/model" | ||
"main/repository" | ||
) | ||
|
||
type applicationService struct { | ||
Repository *repository.Repository | ||
} | ||
|
||
func NewApplicationService(r *repository.Repository) ApplicationService { | ||
return &applicationService{ | ||
Repository: r, | ||
} | ||
} | ||
|
||
func (s *applicationService) GetApplicationById(id string) (*model.Application, error) { | ||
application, err := s.Repository.Application.GetApplicationById(id) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return application, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package template | ||
|
||
import ( | ||
"main/model" | ||
"text/template" | ||
) | ||
|
||
type TemplateService interface { | ||
UseTemplate(page, path string, user model.AzureUser, pageData interface{}) (*template.Template, *model.MasterPageData) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package template | ||
|
||
import ( | ||
"fmt" | ||
"main/config" | ||
"main/model" | ||
"strings" | ||
"text/template" | ||
) | ||
|
||
type templateService struct { | ||
LinkFooters string | ||
OrganizationName string | ||
} | ||
|
||
func NewTemplateService(config config.ConfigManager) TemplateService { | ||
return &templateService{ | ||
LinkFooters: config.GetLinkFooters(), | ||
OrganizationName: config.GetOrganizationName(), | ||
} | ||
} | ||
|
||
func (t *templateService) UseTemplate(page, path string, user model.AzureUser, pageData interface{}) (*template.Template, *model.MasterPageData) { | ||
// Data on master page | ||
var menu []model.Menu | ||
menu = append(menu, model.Menu{Name: "My Requests", Url: "/", IconPath: "/public/icons/projects.svg"}) | ||
menu = append(menu, model.Menu{Name: "My Approvals", Url: "/myapprovals", IconPath: "/public/icons/approvals.svg"}) | ||
masterPageData := model.Headers{Menu: menu, Page: getUrlPath(path)} | ||
|
||
//Footers | ||
var footers []model.Footer | ||
footerString := t.LinkFooters | ||
res := strings.Split(footerString, ";") | ||
for _, footer := range res { | ||
f := strings.Split(footer, ">") | ||
footers = append(footers, model.Footer{Text: f[0], Url: f[1]}) | ||
} | ||
|
||
data := model.MasterPageData{ | ||
Header: masterPageData, | ||
Profile: user, | ||
Content: pageData, | ||
Footers: footers, | ||
OrganizationName: t.OrganizationName, | ||
} | ||
|
||
tmpl := template.Must( | ||
template.ParseFiles("templates/master.html", "templates/buttons.html", | ||
fmt.Sprintf("templates/%v.html", page))) | ||
|
||
return tmpl, &data | ||
} | ||
|
||
func getUrlPath(path string) string { | ||
p := strings.Split(path, "/") | ||
if p[1] == "" { | ||
return "/" | ||
} else { | ||
return fmt.Sprintf("/%s", p[1]) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package timedjobs | ||
|
||
type TimedJobs interface { | ||
ReprocessFailedCallbacks() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
package timedjobs | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"main/config" | ||
"main/model" | ||
"main/service" | ||
"net/http" | ||
"strconv" | ||
"time" | ||
) | ||
|
||
type timedJobs struct { | ||
Service *service.Service | ||
configManager config.ConfigManager | ||
} | ||
|
||
func NewTimedJobs(s *service.Service, configManager config.ConfigManager) TimedJobs { | ||
return &timedJobs{ | ||
Service: s, | ||
configManager: configManager, | ||
} | ||
} | ||
|
||
func (t *timedJobs) ReprocessFailedCallbacks() { | ||
freq := t.configManager.GetCallbackRetryFreq() | ||
freqInt, _ := strconv.ParseInt(freq, 0, 64) | ||
if freqInt > 0 { | ||
for range time.NewTicker(time.Duration(freqInt) * time.Minute).C { | ||
f, err := t.Service.Item.GetFailedCallbacks() | ||
if err != nil { | ||
fmt.Printf("Failed to get failed callbacks: %v", err.Error()) | ||
return | ||
} | ||
|
||
for _, id := range f { | ||
go t.postCallback(id) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (t *timedJobs) postCallback(id string) { | ||
item, err := t.Service.Item.GetItemById(id) | ||
if err != nil { | ||
fmt.Println("Error getting item by id: ", id) | ||
return | ||
} | ||
|
||
if item.CallbackUrl == "" { | ||
fmt.Println("No callback url found") | ||
return | ||
} else { | ||
params := model.ResponseCallback{ | ||
ItemId: id, | ||
IsApproved: item.IsApproved, | ||
Remarks: item.ApproverRemarks, | ||
ResponseDate: item.DateResponded, | ||
RespondedBy: item.RespondedBy, | ||
} | ||
|
||
jsonReq, err := json.Marshal(params) | ||
if err != nil { | ||
return | ||
} | ||
|
||
res, err := http.Post(item.CallbackUrl, "application/json", bytes.NewBuffer(jsonReq)) | ||
if err != nil { | ||
fmt.Println("Error posting callback: ", err) | ||
return | ||
} | ||
|
||
isCallbackFailed := res.StatusCode != 200 | ||
|
||
err = t.Service.Item.UpdateItemCallback(id, isCallbackFailed) | ||
if err != nil { | ||
fmt.Println("Error updating item callback: ", err) | ||
return | ||
} | ||
} | ||
} |
2 changes: 1 addition & 1 deletion
2
...d Procedures/PR_RESPONSE_IsAuthorized.sql → ...ored Procedures/PR_Items_IsAuthorized.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters