From 41282bb1b45d49a6fbb3a413a134d472a7245462 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 29 Apr 2015 15:28:55 +0200 Subject: [PATCH 1/8] Implemented "contentFor" and "block" helpers. --- structure/requestdata.go | 5 +++-- templates/generation.go | 4 ++-- templates/handlebars.go | 24 ++++++++++++++++++++++++ templates/helperfunctions.go | 2 ++ watcher/watcher.go | 3 ++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/structure/requestdata.go b/structure/requestdata.go index 6f46567e..de292025 100644 --- a/structure/requestdata.go +++ b/structure/requestdata.go @@ -13,6 +13,7 @@ type RequestData struct { CurrentIndexPage int CurrentPostIndex int CurrentTagIndex int - CurrentHelperContext int // 0 = index, 1 = post, 2 = tag, 3 = author - used by block helpers - CurrentTemplate int // 0 = index, 1 = post, 2 = tag, 3 = author - never changes during execution. Used by funcs like body_classFunc etc to output the correct class + CurrentHelperContext int // 0 = index, 1 = post, 2 = tag, 3 = author - used by block helpers + CurrentTemplate int // 0 = index, 1 = post, 2 = tag, 3 = author - never changes during execution. Used by funcs like body_classFunc etc to output the correct class + ContentForHelpers []Helper // contentFor helpers that are attached to the currently rendering helper } diff --git a/templates/generation.go b/templates/generation.go index 70eea741..8ec2e01a 100644 --- a/templates/generation.go +++ b/templates/generation.go @@ -170,10 +170,10 @@ func compileTemplate(data []byte, name string) *structure.Helper { data, allHelpers = findHelper(data, allHelpers) baseHelper.Block = data baseHelper.Children = allHelpers - // Handle extend helper + // Handle extend and contentFor helpers for index, child := range baseHelper.Children { if child.Name == "body" { - baseHelper.BodyHelper = &baseHelper.Children[index] + baseHelper.BodyHelper = &baseHelper.Children[index] //TODO: This handles only one body helper per hbs file. That is a potential bug source, but no theme should be using more than one per file anyway. } } return &baseHelper diff --git a/templates/handlebars.go b/templates/handlebars.go index b5d6f16a..da3be161 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -43,6 +43,30 @@ func nullFunc(helper *structure.Helper, values *structure.RequestData) []byte { return []byte{} } +func contentForFunc(helper *structure.Helper, values *structure.RequestData) []byte { + // If there is no array attached to the request data already, make one + if values.ContentForHelpers == nil { + values.ContentForHelpers = make([]structure.Helper, 0) + } + // Collect all contentFor helpers to use them with a block helper + values.ContentForHelpers = append(values.ContentForHelpers, *helper) + return []byte{} +} + +func blockFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if len(helper.Arguments) != 0 { + // Loop through the collected contentFor helpers and execute the appropriate one + for index, _ := range values.ContentForHelpers { + if len(values.ContentForHelpers[index].Arguments) != 0 { + if values.ContentForHelpers[index].Arguments[0].Name == helper.Arguments[0].Name { + return executeHelper(&values.ContentForHelpers[index], values, values.CurrentHelperContext) + } + } + } + } + return []byte{} +} + func paginationDotTotalFunc(helper *structure.Helper, values *structure.RequestData) []byte { if values.CurrentTemplate == 0 { // index return []byte(strconv.FormatInt(values.Blog.PostCount, 10)) diff --git a/templates/helperfunctions.go b/templates/helperfunctions.go index 687a0729..b5883730 100644 --- a/templates/helperfunctions.go +++ b/templates/helperfunctions.go @@ -27,6 +27,8 @@ var helperFuctions = map[string]func(*structure.Helper, *structure.RequestData) "plural": pluralFunc, "date": dateFunc, "image": imageFunc, + "contentFor": contentForFunc, + "block": blockFunc, // @blog functions "@blog.title": atBlogDotTitleFunc, diff --git a/watcher/watcher.go b/watcher/watcher.go index 511e82da..7180e22e 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -1,6 +1,7 @@ package watcher import ( + "github.com/kabukky/journey/helpers" "gopkg.in/fsnotify.v1" "log" "os" @@ -58,7 +59,7 @@ func createWatcher(extensionsFunctions map[string]func() error) (*fsnotify.Watch case event := <-watcher.Events: if event.Op&fsnotify.Write == fsnotify.Write { for key, value := range extensionsFunctions { - if filepath.Ext(event.Name) == key { + if !helpers.IsDirectory(event.Name) && filepath.Ext(event.Name) == key { // Call the function associated with this file extension err := value() if err != nil { From 5c74d930e22f05a60d30ef02893871fc8c9ce362 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 29 Apr 2015 16:07:35 +0200 Subject: [PATCH 2/8] Fixed author and name.name helpers. --- templates/generation.go | 2 +- templates/handlebars.go | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/templates/generation.go b/templates/generation.go index 8ec2e01a..5c783809 100644 --- a/templates/generation.go +++ b/templates/generation.go @@ -170,7 +170,7 @@ func compileTemplate(data []byte, name string) *structure.Helper { data, allHelpers = findHelper(data, allHelpers) baseHelper.Block = data baseHelper.Children = allHelpers - // Handle extend and contentFor helpers + // Handle extend helpers for index, child := range baseHelper.Children { if child.Name == "body" { baseHelper.BodyHelper = &baseHelper.Children[index] //TODO: This handles only one body helper per hbs file. That is a potential bug source, but no theme should be using more than one per file anyway. diff --git a/templates/handlebars.go b/templates/handlebars.go index da3be161..5e034157 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -368,7 +368,14 @@ func authorFunc(helper *structure.Helper, values *structure.RequestData) []byte if len(helper.Block) != 0 { return executeHelper(helper, values, 3) // context = author } - // Else return author.name + // Else return author name (as link) + arguments := methods.ProcessHelperArguments(helper.Arguments) + for key, value := range arguments { + // If link is set to false, just return the name + if key == "autolink" && value == "false" { + return evaluateEscape(values.Posts[values.CurrentPostIndex].Author.Name, helper.Unescaped) + } + } var buffer bytes.Buffer buffer.WriteString("") - // TODO: Error handling if there is no Posts[values.CurrentPostIndex] - buffer.Write(evaluateEscape(values.Posts[values.CurrentPostIndex].Author.Name, helper.Unescaped)) - buffer.WriteString("") - return buffer.Bytes() + return evaluateEscape(values.Posts[values.CurrentPostIndex].Author.Name, helper.Unescaped) } func bioFunc(helper *structure.Helper, values *structure.RequestData) []byte { From 7332226beab2589a366928be9d00af29c2a7a6f8 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 29 Apr 2015 16:22:22 +0200 Subject: [PATCH 3/8] last_login is now updated in database whenever a user logs in. --- database/update.go | 15 +++++++++++++++ server/admin.go | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/database/update.go b/database/update.go index fd59550a..b627b47c 100644 --- a/database/update.go +++ b/database/update.go @@ -8,6 +8,7 @@ const stmtUpdatePost = "UPDATE posts SET title = ?, slug = ?, markdown = ?, html const stmtUpdatePostPublished = "UPDATE posts SET title = ?, slug = ?, markdown = ?, html = ?, featured = ?, page = ?, status = ?, image = ?, updated_at = ?, updated_by = ?, published_at = ?, published_by = ? WHERE id = ?" const stmtUpdateSettings = "UPDATE settings SET value = ?, updated_at = ?, updated_by = ? WHERE key = ?" const stmtUpdateUser = "UPDATE users SET email = ?, image = ?, cover = ?, bio = ?, website = ?, location = ?, updated_at = ?, updated_by = ? WHERE id = ?" +const stmtUpdateLastLogin = "UPDATE users SET last_login = ? WHERE id = ?" const stmtUpdateUserPassword = "UPDATE users SET password = ?, updated_at = ?, updated_by = ? WHERE id = ?" func UpdatePost(id int64, title []byte, slug string, markdown []byte, html []byte, featured bool, isPage bool, published bool, image []byte, updated_at time.Time, updated_by int64) error { @@ -110,6 +111,20 @@ func UpdateUser(id int64, email []byte, image []byte, cover []byte, bio []byte, return writeDB.Commit() } +func UpdateLastLogin(date time.Time, userId int64) error { + writeDB, err := readDB.Begin() + if err != nil { + writeDB.Rollback() + return err + } + _, err = writeDB.Exec(stmtUpdateLastLogin, date, userId) + if err != nil { + writeDB.Rollback() + return err + } + return writeDB.Commit() +} + func UpdateUserPassword(id int64, password string, updated_at time.Time, updated_by int64) error { writeDB, err := readDB.Begin() if err != nil { diff --git a/server/admin.go b/server/admin.go index c060f3e8..41e5664a 100644 --- a/server/admin.go +++ b/server/admin.go @@ -85,6 +85,14 @@ func postLoginHandler(w http.ResponseWriter, r *http.Request, _ map[string]strin if name != "" && password != "" { if authentication.LoginIsCorrect(name, password) { authentication.SetSession(name, w) + userId, err := getUserId(name) + if err != nil { + log.Println("Couldn't get id of logged in user:", err) + } + err = database.UpdateLastLogin(time.Now(), userId) + if err != nil { + log.Println("Couldn't update last login date of a user:", err) + } } else { log.Println("Failed login attempt for user " + name) } From a9b4531fa0ff041f09c13c35e9ac2136588c3e6f Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 6 May 2015 11:57:51 +0200 Subject: [PATCH 4/8] See description for Changes. - Added: navigation.hbs and pagination.hbs are now read from theme files. If they are not present, the built-in ones will be used. - Added: Navigation helpers and interface in the settings (admin interface). - Changed: blog data is now a global variable with RWMutex. --- built-in/admin/admin-angular.js | 58 ++++++++++++- built-in/admin/admin.css | 8 ++ built-in/admin/login.html | 2 + built-in/admin/settings.html | 29 +++++++ built-in/hbs/navigation.hbs | 5 ++ built-in/hbs/pagination.hbs | 9 ++ database/initialization.go | 78 ++++++++--------- database/retrieval.go | 22 +++++ database/update.go | 8 +- filenames/filenames.go | 1 + main.go | 8 ++ server/admin.go | 56 ++++++++---- server/blog.go | 27 +++--- server/pages.go | 2 +- slug/slug.go | 2 +- structure/blog.go | 24 ++++-- structure/methods/blog.go | 39 ++++++++- structure/methods/post.go | 24 ++++++ structure/navigation.go | 8 ++ structure/requestdata.go | 22 ++--- templates/generation.go | 28 +++++- templates/handlebars.go | 145 ++++++++++++++------------------ templates/helperfunctions.go | 23 +++-- templates/templates.go | 54 ++++++------ 24 files changed, 460 insertions(+), 222 deletions(-) create mode 100644 built-in/hbs/navigation.hbs create mode 100644 built-in/hbs/pagination.hbs create mode 100644 structure/navigation.go diff --git a/built-in/admin/admin-angular.js b/built-in/admin/admin-angular.js index 6ebcb5e4..d642e582 100644 --- a/built-in/admin/admin-angular.js +++ b/built-in/admin/admin-angular.js @@ -1,5 +1,6 @@ //register the modules (don't forget ui bootstrap and bootstrap switch) var adminApp = angular.module('adminApp', ['ngRoute', 'frapontillo.bootstrap-switch', 'ui.bootstrap', 'infinite-scroll']); + adminApp.config(function($routeProvider) { $routeProvider. when('/', { @@ -35,14 +36,34 @@ adminApp.directive('imgSelectionDirective', function() { return { restrict: 'A', link: function(scope, elem, attrs) { - $(elem).click(function() { - $('.imgselected').removeClass('imgselected'); - $(elem).children('img').addClass('imgselected'); - }); + $(elem).click(function() { + $('.imgselected').removeClass('imgselected'); + $(elem).children('img').addClass('imgselected'); + }); } } }); +//directive to add/remove the blog url to/from the navigation url fields +adminApp.directive('evaluateUrl', function() { + return { + require: 'ngModel', + link: function(scope, elem, attrs, ctrl) { + elem.on('blur', function() { + var value = elem.val(); + //if the url of this item doesn't start with http/https, add the blog url to it. + if (!(value.substring(0, 'http://'.length) === 'http://') && !(value.substring(0, 'https://'.length) === 'https://') && !(value.substring(0, scope.shared.blog.Url.length) === scope.shared.blog.Url)) { + if ((value.substring(0, 1) != '/') && (scope.shared.blog.Url.slice(-1) != '/')) { + value = '/' + value; + } + value = scope.shared.blog.Url + value; + elem.val(value); + } + }); + } + }; +}); + //factory to load items in infinite-scroll adminApp.factory('infiniteScrollFactory', function($http) { var infiniteScrollFactory = function(url) { @@ -93,11 +114,30 @@ adminApp.controller('SettingsCtrl', function ($scope, $http, $timeout, $sce, $lo //change the navbar according to controller $scope.navbarHtml = $sce.trustAsHtml(''); $scope.shared = sharingService.shared; + //variable to hold the field prefix + $scope.prefix = ''; $scope.loadData = function() { $http.get('/admin/api/blog').success(function(data) { $scope.shared.blog = data; + //select active theme var themeIndex = $scope.shared.blog.Themes.indexOf($scope.shared.blog.ActiveTheme); $scope.shared.blog.ActiveTheme = $scope.shared.blog.Themes[themeIndex]; + //make sure NavigationItems is not null + if ($scope.shared.blog.NavigationItems == null) { + $scope.shared.blog.NavigationItems = [] + } + //append the blog url to the navigation items if necessary + for (var i = 0; i < $scope.shared.blog.NavigationItems.length; i++) { + var value = $scope.shared.blog.NavigationItems[i].url; + //if the url of this item doesn't start with http/https, add the blog url to it. + if (!(value.substring(0, 'http://'.length) === 'http://') && !(value.substring(0, 'https://'.length) === 'https://') && !(value.substring(0, $scope.shared.blog.Url.length) === $scope.shared.blog.Url)) { + if ((value.substring(0, 1) != '/') && ($scope.shared.blog.Url.slice(-1) != '/')) { + value = '/' + value; + } + value = $scope.shared.blog.Url + value; + $scope.shared.blog.NavigationItems[i].url = value; + } + } }); $http.get('/admin/api/userid').success(function(data) { $scope.authenticatedUser = data; @@ -107,6 +147,16 @@ adminApp.controller('SettingsCtrl', function ($scope, $http, $timeout, $sce, $lo }); }; $scope.loadData(); + $scope.deleteNavItem = function(index) { + $scope.shared.blog.NavigationItems.splice(index, 1); + }; + $scope.addNavItem = function() { + var url = $scope.shared.blog.Url + if (url.slice(-1) != '/') { + url = url + '/'; + } + $scope.shared.blog['NavigationItems'].push({label: 'Home', url: url}); + }; $scope.save = function() { $http.patch('/admin/api/blog', $scope.shared.blog); $http.patch('/admin/api/user', $scope.shared.user).success(function(data) { diff --git a/built-in/admin/admin.css b/built-in/admin/admin.css index 992bd9ce..9d9be536 100644 --- a/built-in/admin/admin.css +++ b/built-in/admin/admin.css @@ -147,4 +147,12 @@ img { .navbar-label-text { font-size: 16px; color: #ffffff; +} +.navigation-item { + padding-top: 10px; + padding-bottom: 10px; + background-color: #f8f8f8; + outline: 1px solid #dddddd; + margin-right: 0 !important; + margin-left: 0 !important; } \ No newline at end of file diff --git a/built-in/admin/login.html b/built-in/admin/login.html index e77ed5bd..541816f3 100644 --- a/built-in/admin/login.html +++ b/built-in/admin/login.html @@ -4,6 +4,8 @@ Admin Area + + diff --git a/built-in/admin/settings.html b/built-in/admin/settings.html index 8f7c3942..124a8434 100644 --- a/built-in/admin/settings.html +++ b/built-in/admin/settings.html @@ -55,6 +55,35 @@

Blog

+ +
+ +
+
+ +
+
+
diff --git a/built-in/hbs/navigation.hbs b/built-in/hbs/navigation.hbs new file mode 100644 index 00000000..1d302698 --- /dev/null +++ b/built-in/hbs/navigation.hbs @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/built-in/hbs/pagination.hbs b/built-in/hbs/pagination.hbs new file mode 100644 index 00000000..67d44efb --- /dev/null +++ b/built-in/hbs/pagination.hbs @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/database/initialization.go b/database/initialization.go index a744e25f..fca7d948 100644 --- a/database/initialization.go +++ b/database/initialization.go @@ -10,69 +10,70 @@ import ( "time" ) -var readDB *sql.DB // Handler for read access +// Handler for read access +var readDB *sql.DB var stmtInitialization = `CREATE TABLE IF NOT EXISTS posts ( - id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, uuid varchar(36) NOT NULL, - title varchar(150) NOT NULL, + title varchar(150) NOT NULL, slug varchar(150) NOT NULL, markdown text, html text, - image text, + image text, featured tinyint NOT NULL DEFAULT '0', page tinyint NOT NULL DEFAULT '0', - status varchar(150) NOT NULL DEFAULT 'draft', + status varchar(150) NOT NULL DEFAULT 'draft', language varchar(6) NOT NULL DEFAULT 'en_US', - meta_title varchar(150), + meta_title varchar(150), meta_description varchar(200), - author_id integer NOT NULL, - created_at datetime NOT NULL, - created_by integer NOT NULL, - updated_at datetime, - updated_by integer, + author_id integer NOT NULL, + created_at datetime NOT NULL, + created_by integer NOT NULL, + updated_at datetime, + updated_by integer, published_at datetime, published_by integer ); CREATE TABLE IF NOT EXISTS users ( - id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, uuid varchar(36) NOT NULL, name varchar(150) NOT NULL, slug varchar(150) NOT NULL, password varchar(60) NOT NULL, - email varchar(254) NOT NULL, - image text, - cover text, - bio varchar(200), - website text, + email varchar(254) NOT NULL, + image text, + cover text, + bio varchar(200), + website text, location text, - accessibility text, - status varchar(150) NOT NULL DEFAULT 'active', + accessibility text, + status varchar(150) NOT NULL DEFAULT 'active', language varchar(6) NOT NULL DEFAULT 'en_US', - meta_title varchar(150), + meta_title varchar(150), meta_description varchar(200), - last_login datetime, - created_at datetime NOT NULL, - created_by integer NOT NULL, - updated_at datetime, - updated_by integer + last_login datetime, + created_at datetime NOT NULL, + created_by integer NOT NULL, + updated_at datetime, + updated_by integer ); CREATE TABLE IF NOT EXISTS tags ( - id integer NOT NULL PRIMARY KEY AUTOINCREMENT, + id integer NOT NULL PRIMARY KEY AUTOINCREMENT, uuid varchar(36) NOT NULL, name varchar(150) NOT NULL, slug varchar(150) NOT NULL, - description varchar(200), - parent_id integer, - meta_title varchar(150), + description varchar(200), + parent_id integer, + meta_title varchar(150), meta_description varchar(200), - created_at datetime NOT NULL, - created_by integer NOT NULL, - updated_at datetime, - updated_by integer + created_at datetime NOT NULL, + created_by integer NOT NULL, + updated_at datetime, + updated_by integer ); CREATE TABLE IF NOT EXISTS posts_tags ( @@ -83,10 +84,10 @@ var stmtInitialization = `CREATE TABLE IF NOT EXISTS CREATE TABLE IF NOT EXISTS settings ( id integer NOT NULL PRIMARY KEY AUTOINCREMENT, - uuid varchar(36) NOT NULL, + uuid varchar(36) NOT NULL, key varchar(150) NOT NULL, value text, - type varchar(150) NOT NULL DEFAULT 'core', + type varchar(150) NOT NULL DEFAULT 'core', created_at datetime NOT NULL, created_by integer NOT NULL, updated_at datetime, @@ -99,11 +100,12 @@ var stmtInitialization = `CREATE TABLE IF NOT EXISTS INSERT OR IGNORE INTO settings (id, uuid, key, value, type, created_at, created_by, updated_at, updated_by) VALUES (5, ?, 'cover', '/public/images/blog-cover.jpg', 'blog', ?, 1, ?, 1); INSERT OR IGNORE INTO settings (id, uuid, key, value, type, created_at, created_by, updated_at, updated_by) VALUES (6, ?, 'postsPerPage', 5, 'blog', ?, 1, ?, 1); INSERT OR IGNORE INTO settings (id, uuid, key, value, type, created_at, created_by, updated_at, updated_by) VALUES (7, ?, 'activeTheme', 'promenade', 'theme', ?, 1, ?, 1); + INSERT OR IGNORE INTO settings (id, uuid, key, value, type, created_at, created_by, updated_at, updated_by) VALUES (8, ?, 'navigation', '[{"label":"Home", "url":"/"}]', 'blog', ?, 1, ?, 1); CREATE TABLE IF NOT EXISTS roles ( id integer NOT NULL PRIMARY KEY AUTOINCREMENT, - uuid varchar(36) NOT NULL, - name varchar(150) NOT NULL, + uuid varchar(36) NOT NULL, + name varchar(150) NOT NULL, description varchar(200), created_at datetime NOT NULL, created_by integer NOT NULL, @@ -140,7 +142,7 @@ func Initialize() error { return err } currentTime := time.Now() - _, err = readDB.Exec(stmtInitialization, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime) + _, err = readDB.Exec(stmtInitialization, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime, uuid.Formatter(uuid.NewV4(), uuid.CleanHyphen), currentTime, currentTime) // TODO: Is Commit()/Rollback() needed for DB.Exec()? if err != nil { return err diff --git a/database/retrieval.go b/database/retrieval.go index e420f35e..90e96b4c 100644 --- a/database/retrieval.go +++ b/database/retrieval.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "encoding/json" "github.com/kabukky/journey/structure" "time" ) @@ -347,11 +348,23 @@ func RetrieveBlog() (*structure.Blog, error) { if err != nil { return &tempBlog, err } + // Post count postCount, err := RetrieveNumberOfPosts() if err != nil { return &tempBlog, err } tempBlog.PostCount = postCount + // Navigation + var navigation []byte + row = readDB.QueryRow(stmtRetrieveBlog, "navigation") + err = row.Scan(&navigation) + if err != nil { + return &tempBlog, err + } + tempBlog.NavigationItems, err = makeNavigation(navigation) + if err != nil { + return &tempBlog, err + } return &tempBlog, err } @@ -374,3 +387,12 @@ func RetrieveUsersCount() int { } return userCount } + +func makeNavigation(navigation []byte) ([]structure.Navigation, error) { + navigationItems := make([]structure.Navigation, 0) + err := json.Unmarshal(navigation, &navigationItems) + if err != nil { + return navigationItems, err + } + return navigationItems, nil +} diff --git a/database/update.go b/database/update.go index b627b47c..41e98f4f 100644 --- a/database/update.go +++ b/database/update.go @@ -38,7 +38,7 @@ func UpdatePost(id int64, title []byte, slug string, markdown []byte, html []byt return writeDB.Commit() } -func UpdateSettings(title []byte, description []byte, logo []byte, cover []byte, postsPerPage int64, activeTheme string, updated_at time.Time, updated_by int64) error { +func UpdateSettings(title []byte, description []byte, logo []byte, cover []byte, postsPerPage int64, activeTheme string, navigation []byte, updated_at time.Time, updated_by int64) error { writeDB, err := readDB.Begin() if err != nil { writeDB.Rollback() @@ -80,6 +80,12 @@ func UpdateSettings(title []byte, description []byte, logo []byte, cover []byte, writeDB.Rollback() return err } + // Navigation + _, err = writeDB.Exec(stmtUpdateSettings, navigation, updated_at, updated_by, "navigation") + if err != nil { + writeDB.Rollback() + return err + } return writeDB.Commit() } diff --git a/filenames/filenames.go b/filenames/filenames.go index 4278165b..a171d781 100644 --- a/filenames/filenames.go +++ b/filenames/filenames.go @@ -31,6 +31,7 @@ var ( //For built-in files (e.g. the admin interface) AdminFilepath = filepath.Join("built-in", "admin") PublicFilepath = filepath.Join("built-in", "public") + HbsFilepath = filepath.Join("built-in", "hbs") // For handlebars (this is a url string) JqueryFilename = "/public/jquery/jquery.js" diff --git a/main.go b/main.go index c2d6c198..457a662f 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/kabukky/journey/flags" "github.com/kabukky/journey/plugins" "github.com/kabukky/journey/server" + "github.com/kabukky/journey/structure/methods" "github.com/kabukky/journey/templates" "log" "net/http" @@ -60,6 +61,13 @@ func main() { return } + // Global blog data + err = methods.GenerateBlog() + if err != nil { + log.Fatal("Error: Couldn't generate blog data: " + err.Error()) + return + } + // Templates err = templates.Generate() if err != nil { diff --git a/server/admin.go b/server/admin.go index 41e5664a..2bf7c865 100644 --- a/server/admin.go +++ b/server/admin.go @@ -38,13 +38,15 @@ type JsonPost struct { } type JsonBlog struct { - Title string - Description string - Logo string - Cover string - Themes []string - ActiveTheme string - PostsPerPage int64 + Url string + Title string + Description string + Logo string + Cover string + Themes []string + ActiveTheme string + PostsPerPage int64 + NavigationItems []structure.Navigation } type JsonUser struct { @@ -333,7 +335,7 @@ func deleteApiPostHandler(w http.ResponseWriter, r *http.Request, params map[str http.Error(w, err.Error(), http.StatusInternalServerError) return } - err = database.DeletePostById(postId) + err = methods.DeletePost(postId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -498,12 +500,10 @@ func deleteApiImageHandler(w http.ResponseWriter, r *http.Request, _ map[string] func getApiBlogHandler(w http.ResponseWriter, r *http.Request, _ map[string]string) { userName := authentication.GetUserName(r) if userName != "" { - blog, err := database.RetrieveBlog() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - blogJson := JsonBlog{Title: string(blog.Title), Description: string(blog.Description), Logo: string(blog.Logo), Cover: string(blog.Cover), PostsPerPage: blog.PostsPerPage, Themes: templates.GetAllThemes(), ActiveTheme: blog.ActiveTheme} + // Read lock the global blog + methods.Blog.RLock() + defer methods.Blog.RUnlock() + blogJson := blogToJson(methods.Blog) json, err := json.Marshal(blogJson) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -538,13 +538,23 @@ func patchApiBlogHandler(w http.ResponseWriter, r *http.Request, _ map[string]st if json.PostsPerPage < 1 { json.PostsPerPage = 1 } - // Retrieve old post settings for comparison + // Remove blog url in front of navigation urls + for index, _ := range json.NavigationItems { + if strings.HasPrefix(json.NavigationItems[index].Url, json.Url) { + json.NavigationItems[index].Url = strings.Replace(json.NavigationItems[index].Url, json.Url, "", 1) + // If we removed the blog url, there should be a / in front of the url + if !strings.HasPrefix(json.NavigationItems[index].Url, "/") { + json.NavigationItems[index].Url = "/" + json.NavigationItems[index].Url + } + } + } + // Retrieve old blog settings for comparison blog, err := database.RetrieveBlog() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - tempBlog := structure.Blog{Url: []byte(configuration.Config.Url), Title: []byte(json.Title), Description: []byte(json.Description), Logo: []byte(json.Logo), Cover: []byte(json.Cover), AssetPath: []byte("/assets/"), PostCount: blog.PostCount, PostsPerPage: json.PostsPerPage, ActiveTheme: json.ActiveTheme} + tempBlog := structure.Blog{Url: []byte(configuration.Config.Url), Title: []byte(json.Title), Description: []byte(json.Description), Logo: []byte(json.Logo), Cover: []byte(json.Cover), AssetPath: []byte("/assets/"), PostCount: blog.PostCount, PostsPerPage: json.PostsPerPage, ActiveTheme: json.ActiveTheme, NavigationItems: json.NavigationItems} err = methods.UpdateBlog(&tempBlog, userId) // Check if active theme setting has been changed, if so, generate templates from new theme if tempBlog.ActiveTheme != blog.ActiveTheme { @@ -721,6 +731,20 @@ func postToJson(post *structure.Post) *JsonPost { return &jsonPost } +func blogToJson(blog *structure.Blog) *JsonBlog { + var jsonBlog JsonBlog + jsonBlog.Url = string(blog.Url) + jsonBlog.Title = string(blog.Title) + jsonBlog.Description = string(blog.Description) + jsonBlog.Logo = string(blog.Logo) + jsonBlog.Cover = string(blog.Cover) + jsonBlog.PostsPerPage = blog.PostsPerPage + jsonBlog.Themes = templates.GetAllThemes() + jsonBlog.ActiveTheme = blog.ActiveTheme + jsonBlog.NavigationItems = blog.NavigationItems + return &jsonBlog +} + func InitializeAdmin(router *httptreemux.TreeMux) { // For admin panel router.GET("/admin/", adminHandler) diff --git a/server/blog.go b/server/blog.go index b543ee97..1f59ab92 100644 --- a/server/blog.go +++ b/server/blog.go @@ -2,8 +2,8 @@ package server import ( "github.com/dimfeld/httptreemux" - "github.com/kabukky/journey/database" "github.com/kabukky/journey/filenames" + "github.com/kabukky/journey/structure/methods" "github.com/kabukky/journey/templates" "net/http" "path/filepath" @@ -14,7 +14,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request, params map[string]stri number := params["number"] if number == "" { // Render index template (first page) - err := templates.ShowIndexTemplate(w, 1) + err := templates.ShowIndexTemplate(w, r, 1) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -27,7 +27,7 @@ func indexHandler(w http.ResponseWriter, r *http.Request, params map[string]stri return } // Render index template - err = templates.ShowIndexTemplate(w, page) + err = templates.ShowIndexTemplate(w, r, page) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -41,7 +41,7 @@ func authorHandler(w http.ResponseWriter, r *http.Request, params map[string]str number := params["number"] if function == "" { // Render author template (first page) - err := templates.ShowAuthorTemplate(w, slug, 1) + err := templates.ShowAuthorTemplate(w, r, slug, 1) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -62,7 +62,7 @@ func authorHandler(w http.ResponseWriter, r *http.Request, params map[string]str return } // Render author template - err = templates.ShowAuthorTemplate(w, slug, page) + err = templates.ShowAuthorTemplate(w, r, slug, page) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -76,7 +76,7 @@ func tagHandler(w http.ResponseWriter, r *http.Request, params map[string]string number := params["number"] if function == "" { // Render tag template (first page) - err := templates.ShowTagTemplate(w, slug, 1) + err := templates.ShowTagTemplate(w, r, slug, 1) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -97,7 +97,7 @@ func tagHandler(w http.ResponseWriter, r *http.Request, params map[string]string return } // Render tag template - err = templates.ShowTagTemplate(w, slug, page) + err = templates.ShowTagTemplate(w, r, slug, page) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -120,7 +120,7 @@ func postHandler(w http.ResponseWriter, r *http.Request, params map[string]strin return } // Render post template - err := templates.ShowPostTemplate(w, slug) + err := templates.ShowPostTemplate(w, r, slug) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -129,13 +129,10 @@ func postHandler(w http.ResponseWriter, r *http.Request, params map[string]strin } func assetsHandler(w http.ResponseWriter, r *http.Request, params map[string]string) { - // TODO: It might be possible to do this more efficently. Getting the theme from the database at every request seems like too much. - activeTheme, err := database.RetrieveActiveTheme() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - http.ServeFile(w, r, filepath.Join(filenames.ThemesFilepath, *activeTheme, "assets", params["filepath"])) + // Read lock global blog + methods.Blog.RLock() + defer methods.Blog.RUnlock() + http.ServeFile(w, r, filepath.Join(filenames.ThemesFilepath, methods.Blog.ActiveTheme, "assets", params["filepath"])) return } diff --git a/server/pages.go b/server/pages.go index 341a50bd..c077e8ce 100644 --- a/server/pages.go +++ b/server/pages.go @@ -14,5 +14,5 @@ func pagesHandler(w http.ResponseWriter, r *http.Request, params map[string]stri func InitializePages(router *httptreemux.TreeMux) { // For serving standalone projects or pages saved in in content/pages - router.GET("/pages/*filepath", pagesHandler) + router.GET("/pages/*filepath/", pagesHandler) } diff --git a/slug/slug.go b/slug/slug.go index 8f54163b..6b398615 100644 --- a/slug/slug.go +++ b/slug/slug.go @@ -34,7 +34,7 @@ func Generate(input string, table string) string { // Don't allow a few specific slugs that are used by the blog if table == "posts" && (output == "rss" || output == "tag" || output == "author" || output == "page" || output == "admin") { output = generateUniqueSlug(output, table, 2) - } else if table == "tags" { // We want duplicate tag slugs + } else if table == "tags" || table == "navigation" { // We want duplicate tag and navigation slugs return output } return generateUniqueSlug(output, table, 1) diff --git a/structure/blog.go b/structure/blog.go index e15ed43e..37c8e032 100644 --- a/structure/blog.go +++ b/structure/blog.go @@ -1,14 +1,20 @@ package structure +import ( + "sync" +) + // Blog: settings that are used for template execution type Blog struct { - Url []byte - Title []byte - Description []byte - Logo []byte - Cover []byte - AssetPath []byte - PostCount int64 - PostsPerPage int64 - ActiveTheme string + sync.RWMutex + Url []byte + Title []byte + Description []byte + Logo []byte + Cover []byte + AssetPath []byte + PostCount int64 + PostsPerPage int64 + ActiveTheme string + NavigationItems []Navigation } diff --git a/structure/methods/blog.go b/structure/methods/blog.go index dc89e839..d90bd994 100644 --- a/structure/methods/blog.go +++ b/structure/methods/blog.go @@ -1,19 +1,35 @@ package methods import ( + "encoding/json" "github.com/kabukky/journey/configuration" "github.com/kabukky/journey/database" + "github.com/kabukky/journey/slug" "github.com/kabukky/journey/structure" + "log" "time" ) +// Global blog - thread safe and accessible by all requests +var Blog *structure.Blog + var assetPath = []byte("/assets/") func UpdateBlog(b *structure.Blog, userId int64) error { - err := database.UpdateSettings(b.Title, b.Description, b.Logo, b.Cover, b.PostsPerPage, b.ActiveTheme, time.Now(), userId) + // Marshal navigation items to json string + navigation, err := json.Marshal(b.NavigationItems) + if err != nil { + return err + } + err = database.UpdateSettings(b.Title, b.Description, b.Logo, b.Cover, b.PostsPerPage, b.ActiveTheme, navigation, time.Now(), userId) if err != nil { return err } + // Generate new global blog + err = GenerateBlog() + if err != nil { + log.Panic("Error: couldn't generate blog data:", err) + } return nil } @@ -22,17 +38,32 @@ func UpdateActiveTheme(activeTheme string, userId int64) error { if err != nil { return err } + // Generate new global blog + err = GenerateBlog() + if err != nil { + log.Panic("Error: couldn't generate blog data:", err) + } return nil } -func GenerateBlog() (*structure.Blog, error) { +func GenerateBlog() error { + // Write lock the global blog + if Blog != nil { + Blog.Lock() + defer Blog.Unlock() + } // Generate blog from db blog, err := database.RetrieveBlog() if err != nil { - return nil, err + return err } // Add parameters that are not saved in db blog.Url = []byte(configuration.Config.Url) blog.AssetPath = assetPath - return blog, nil + // Create navigation slugs + for index, _ := range blog.NavigationItems { + blog.NavigationItems[index].Slug = slug.Generate(blog.NavigationItems[index].Label, "navigation") + } + Blog = blog + return nil } diff --git a/structure/methods/post.go b/structure/methods/post.go index 16d09024..aeda0e06 100644 --- a/structure/methods/post.go +++ b/structure/methods/post.go @@ -3,6 +3,7 @@ package methods import ( "github.com/kabukky/journey/database" "github.com/kabukky/journey/structure" + "log" "time" ) @@ -35,6 +36,11 @@ func SavePost(p *structure.Post) error { return err } } + // Generate new global blog + err = GenerateBlog() + if err != nil { + log.Panic("Error: couldn't generate blog data:", err) + } return nil } @@ -72,5 +78,23 @@ func UpdatePost(p *structure.Post) error { return err } } + // Generate new global blog + err = GenerateBlog() + if err != nil { + log.Panic("Error: couldn't generate blog data:", err) + } + return nil +} + +func DeletePost(postId int64) error { + err := database.DeletePostById(postId) + if err != nil { + return err + } + // Generate new global blog + err = GenerateBlog() + if err != nil { + log.Panic("Error: couldn't generate blog data:", err) + } return nil } diff --git a/structure/navigation.go b/structure/navigation.go new file mode 100644 index 00000000..5331df02 --- /dev/null +++ b/structure/navigation.go @@ -0,0 +1,8 @@ +package structure + +// Navigation: an entry in the navigation menu +type Navigation struct { + Label string `json:"label"` + Url string `json:"url"` + Slug string `json:"-"` +} diff --git a/structure/requestdata.go b/structure/requestdata.go index de292025..69c5472e 100644 --- a/structure/requestdata.go +++ b/structure/requestdata.go @@ -6,14 +6,16 @@ import ( // RequestData: used for template/helper execution. Contains data specific to the incoming request. type RequestData struct { - PluginVMs map[string]*lua.LState - Posts []Post - Blog *Blog - CurrentTag *Tag - CurrentIndexPage int - CurrentPostIndex int - CurrentTagIndex int - CurrentHelperContext int // 0 = index, 1 = post, 2 = tag, 3 = author - used by block helpers - CurrentTemplate int // 0 = index, 1 = post, 2 = tag, 3 = author - never changes during execution. Used by funcs like body_classFunc etc to output the correct class - ContentForHelpers []Helper // contentFor helpers that are attached to the currently rendering helper + PluginVMs map[string]*lua.LState + Posts []Post + Blog *Blog + CurrentTag *Tag + CurrentIndexPage int + CurrentPostIndex int + CurrentTagIndex int + CurrentNavigationIndex int + CurrentHelperContext int // 0 = index, 1 = post, 2 = tag, 3 = author, 4 = navigation - used by block helpers + CurrentTemplate int // 0 = index, 1 = post, 2 = tag, 3 = author - never changes during execution. Used by funcs like body_classFunc etc to output the correct class + ContentForHelpers []Helper // contentFor helpers that are attached to the currently rendering helper + CurrentPath string // path of the the url of this request } diff --git a/templates/generation.go b/templates/generation.go index 5c783809..80e389ce 100644 --- a/templates/generation.go +++ b/templates/generation.go @@ -12,6 +12,7 @@ import ( "github.com/kabukky/journey/structure/methods" "github.com/kabukky/journey/watcher" "io/ioutil" + "log" "os" "path/filepath" "regexp" @@ -193,13 +194,21 @@ func createTemplateFromFile(filename string) (*structure.Helper, error) { return helper, nil } +func compileFile(fileName string) error { + helper, err := createTemplateFromFile(fileName) + if err != nil { + return err + } + compiledTemplates.m[helper.Name] = helper + return nil +} + func inspectTemplateFile(filePath string, info os.FileInfo, err error) error { if !info.IsDir() && filepath.Ext(filePath) == ".hbs" { - helper, err := createTemplateFromFile(filePath) + err := compileFile(filePath) if err != nil { return err } - compiledTemplates.m[helper.Name] = helper } return nil } @@ -220,6 +229,21 @@ func compileTheme(themePath string) error { if _, ok := compiledTemplates.m["post"]; !ok { return errors.New("Couldn't compile template 'post'. Is post.hbs missing?") } + // Check if pagination and navigation templates have been provided by the theme. + // If not, use the build in ones. + if _, ok := compiledTemplates.m["pagination"]; !ok { + err = compileFile(filepath.Join(filenames.HbsFilepath, "pagination.hbs")) + if err != nil { + log.Println("Warning: Couldn't compile pagination template.") + } + } + if _, ok := compiledTemplates.m["navigation"]; !ok { + err = compileFile(filepath.Join(filenames.HbsFilepath, "navigation.hbs")) + if err != nil { + log.Println("Warning: Couldn't compile navigation template.") + } + + } return nil } diff --git a/templates/handlebars.go b/templates/handlebars.go index 5e034157..976d78aa 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -39,7 +39,44 @@ func nullFunc(helper *structure.Helper, values *structure.RequestData) []byte { values.PluginVMs = nil } } - //log.Println("Warning: This helper is not implemented:", helper.Name) + log.Println("Warning: This helper is not implemented:", helper.Name) + return []byte{} +} + +func slugFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if len(values.Blog.NavigationItems) != 0 { + return evaluateEscape([]byte(values.Blog.NavigationItems[values.CurrentNavigationIndex].Slug), helper.Unescaped) + } + return []byte{} +} + +func currentFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if len(values.Blog.NavigationItems) != 0 { + url := values.Blog.NavigationItems[values.CurrentNavigationIndex].Url + // Since the router rewrites all urls with a trailing slash, add / to url if not already there + if !strings.HasSuffix(url, "/") { + url = url + "/" + } + if values.CurrentPath == url { + return []byte("t") + } + } + return []byte{} +} + +func navigationFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if len(values.Blog.NavigationItems) == 0 { + return []byte{} + } else if templateHelper, ok := compiledTemplates.m["navigation"]; ok { + return executeHelper(templateHelper, values, values.CurrentHelperContext) + } + return []byte{} +} + +func labelFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if len(values.Blog.NavigationItems) != 0 { + return evaluateEscape([]byte(values.Blog.NavigationItems[values.CurrentNavigationIndex].Label), helper.Unescaped) + } return []byte{} } @@ -67,6 +104,13 @@ func blockFunc(helper *structure.Helper, values *structure.RequestData) []byte { return []byte{} } +func paginationFunc(helper *structure.Helper, values *structure.RequestData) []byte { + if templateHelper, ok := compiledTemplates.m["pagination"]; ok { + return executeHelper(templateHelper, values, values.CurrentHelperContext) + } + return []byte{} +} + func paginationDotTotalFunc(helper *structure.Helper, values *structure.RequestData) []byte { if values.CurrentTemplate == 0 { // index return []byte(strconv.FormatInt(values.Blog.PostCount, 10)) @@ -518,7 +562,12 @@ func urlFunc(helper *structure.Helper, values *structure.RequestData) []byte { for key, value := range arguments { if key == "absolute" { if value == "true" { - buffer.Write(values.Blog.Url) + // Only write the blog url if navigation url does not begin with http/https + if values.CurrentHelperContext == 4 && (!strings.HasPrefix(values.Blog.NavigationItems[values.CurrentNavigationIndex].Url, "http://") && !strings.HasPrefix(values.Blog.NavigationItems[values.CurrentNavigationIndex].Url, "https://")) { // navigation + buffer.Write(values.Blog.Url) + } else if values.CurrentHelperContext != 4 { + buffer.Write(values.Blog.Url) + } } } } @@ -534,6 +583,9 @@ func urlFunc(helper *structure.Helper, values *structure.RequestData) []byte { buffer.WriteString(values.Posts[values.CurrentPostIndex].Author.Slug) buffer.WriteString("/") return evaluateEscape(buffer.Bytes(), helper.Unescaped) + } else if values.CurrentHelperContext == 4 { // author + buffer.WriteString(values.Blog.NavigationItems[values.CurrentNavigationIndex].Url) + return evaluateEscape(buffer.Bytes(), helper.Unescaped) } return []byte{} } @@ -726,88 +778,6 @@ func tagDotSlugFunc(helper *structure.Helper, values *structure.RequestData) []b } } -func paginationFunc(helper *structure.Helper, values *structure.RequestData) []byte { - if template, ok := compiledTemplates.m["pagination"]; ok { // If the theme has a pagination.hbs - return executeHelper(template, values, values.CurrentHelperContext) - } - var count int64 - var err error - if values.CurrentTemplate == 0 { // index - count = values.Blog.PostCount - } else if values.CurrentTemplate == 2 { // tag - count, err = database.RetrieveNumberOfPostsByTag(values.CurrentTag.Id) - if err != nil { - log.Println("Couldn't get number of posts for tag", err.Error()) - return []byte{} - } - } else if values.CurrentTemplate == 3 { // author - count, err = database.RetrieveNumberOfPostsByUser(values.Posts[values.CurrentPostIndex].Author.Id) - if err != nil { - log.Println("Couldn't get number of posts for author", err.Error()) - return []byte{} - } - } - if count > values.Blog.PostsPerPage { - maxPages := int64((float64(count) / float64(values.Blog.PostsPerPage)) + 0.5) - var buffer bytes.Buffer - buffer.WriteString("") - return buffer.Bytes() - } else { - return []byte("") - } -} - func idFunc(helper *structure.Helper, values *structure.RequestData) []byte { return []byte(strconv.FormatInt(values.Posts[values.CurrentPostIndex].Id, 10)) } @@ -843,6 +813,13 @@ func foreachFunc(helper *structure.Helper, values *structure.RequestData) []byte //} } return buffer.Bytes() + case "navigation": + var buffer bytes.Buffer + for index, _ := range values.Blog.NavigationItems { + values.CurrentNavigationIndex = index + buffer.Write(executeHelper(helper, values, 4)) // context = navigation + } + return buffer.Bytes() default: return []byte{} } diff --git a/templates/helperfunctions.go b/templates/helperfunctions.go index b5883730..01deaa24 100644 --- a/templates/helperfunctions.go +++ b/templates/helperfunctions.go @@ -16,7 +16,6 @@ var helperFuctions = map[string]func(*structure.Helper, *structure.RequestData) "!<": extendFunc, "body": bodyFunc, "asset": assetFunc, - "pagination": paginationFunc, "encode": encodeFunc, ">": insertFunc, "meta_title": meta_titleFunc, @@ -36,13 +35,13 @@ var helperFuctions = map[string]func(*structure.Helper, *structure.RequestData) "@blog.logo": atBlogDotLogoFunc, "@blog.cover": atBlogDotCoverFunc, "@blog.description": atBlogDotDescriptionFunc, + "@blog.navigation": navigationFunc, // Post functions "post": postFunc, "excerpt": excerptFunc, "title": titleFunc, "content": contentFunc, - "url": urlFunc, "post_class": post_classFunc, "featured": featuredFunc, "id": idFunc, @@ -67,20 +66,28 @@ var helperFuctions = map[string]func(*structure.Helper, *structure.RequestData) "author.cover": coverFunc, "author.location": locationFunc, + // Navigation functions + "navigation": navigationFunc, + "label": labelFunc, + "current": currentFunc, + "slug": slugFunc, + // Multiple block functions "@first": atFirstFunc, "@last": atLastFunc, "@even": atEvenFunc, "@odd": atOddFunc, "name": nameFunc, + "url": urlFunc, // Pagination functions - "prev": prevFunc, - "next": nextFunc, - "page": pageFunc, - "pages": pagesFunc, - "page_url": page_urlFunc, - "pageUrl": page_urlFunc, + "pagination": paginationFunc, + "prev": prevFunc, + "next": nextFunc, + "page": pageFunc, + "pages": pagesFunc, + "page_url": page_urlFunc, + "pageUrl": page_urlFunc, // Possible if arguments "posts": postsFunc, diff --git a/templates/templates.go b/templates/templates.go index 37536e24..c6d3dbdb 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -21,23 +21,22 @@ type Templates struct { func newTemplates() *Templates { return &Templates{m: make(map[string]*structure.Helper)} } -// Global compiled templates - thread safe and accessible from all packages +// Global compiled templates - thread safe and accessible by all requests var compiledTemplates = newTemplates() -func ShowPostTemplate(writer http.ResponseWriter, slug string) error { +func ShowPostTemplate(writer http.ResponseWriter, r *http.Request, slug string) error { + // Read lock templates and global blog compiledTemplates.RLock() defer compiledTemplates.RUnlock() - blog, err := methods.GenerateBlog() - if err != nil { - return err - } + methods.Blog.RLock() + defer methods.Blog.RUnlock() post, err := database.RetrievePostBySlug(slug) if err != nil { return err } else if !post.IsPublished { // Make sure the post is published before rendering it return errors.New("Post not published.") } - requestData := structure.RequestData{Posts: make([]structure.Post, 1), Blog: blog, CurrentTemplate: 1} // CurrentTemplate = post + requestData := structure.RequestData{Posts: make([]structure.Post, 1), Blog: methods.Blog, CurrentTemplate: 1, CurrentPath: r.URL.Path} // CurrentTemplate = post requestData.Posts[0] = *post // If the post is a page and the page template is available, use the page template if post.IsPage { @@ -54,26 +53,25 @@ func ShowPostTemplate(writer http.ResponseWriter, slug string) error { return err } -func ShowAuthorTemplate(writer http.ResponseWriter, slug string, page int) error { +func ShowAuthorTemplate(writer http.ResponseWriter, r *http.Request, slug string, page int) error { + // Read lock templates and global blog compiledTemplates.RLock() defer compiledTemplates.RUnlock() + methods.Blog.RLock() + defer methods.Blog.RUnlock() postIndex := int64(page - 1) if postIndex < 0 { postIndex = 0 } - blog, err := methods.GenerateBlog() - if err != nil { - return err - } author, err := database.RetrieveUserBySlug(slug) if err != nil { return err } - posts, err := database.RetrievePostsByUser(author.Id, blog.PostsPerPage, (blog.PostsPerPage * postIndex)) + posts, err := database.RetrievePostsByUser(author.Id, methods.Blog.PostsPerPage, (methods.Blog.PostsPerPage * postIndex)) if err != nil { return err } - requestData := structure.RequestData{Posts: posts, Blog: blog, CurrentIndexPage: page, CurrentTemplate: 3} // CurrentTemplate = author + requestData := structure.RequestData{Posts: posts, Blog: methods.Blog, CurrentIndexPage: page, CurrentTemplate: 3, CurrentPath: r.URL.Path} // CurrentTemplate = author if template, ok := compiledTemplates.m["author"]; ok { _, err = writer.Write(executeHelper(template, &requestData, 0)) // context = index } else { @@ -86,26 +84,25 @@ func ShowAuthorTemplate(writer http.ResponseWriter, slug string, page int) error return err } -func ShowTagTemplate(writer http.ResponseWriter, slug string, page int) error { +func ShowTagTemplate(writer http.ResponseWriter, r *http.Request, slug string, page int) error { + // Read lock templates and global blog compiledTemplates.RLock() defer compiledTemplates.RUnlock() + methods.Blog.RLock() + defer methods.Blog.RUnlock() postIndex := int64(page - 1) if postIndex < 0 { postIndex = 0 } - blog, err := methods.GenerateBlog() - if err != nil { - return err - } tag, err := database.RetrieveTagBySlug(slug) if err != nil { return err } - posts, err := database.RetrievePostsByTag(tag.Id, blog.PostsPerPage, (blog.PostsPerPage * postIndex)) + posts, err := database.RetrievePostsByTag(tag.Id, methods.Blog.PostsPerPage, (methods.Blog.PostsPerPage * postIndex)) if err != nil { return err } - requestData := structure.RequestData{Posts: posts, Blog: blog, CurrentIndexPage: page, CurrentTag: tag, CurrentTemplate: 2} // CurrentTemplate = tag + requestData := structure.RequestData{Posts: posts, Blog: methods.Blog, CurrentIndexPage: page, CurrentTag: tag, CurrentTemplate: 2, CurrentPath: r.URL.Path} // CurrentTemplate = tag if template, ok := compiledTemplates.m["tag"]; ok { _, err = writer.Write(executeHelper(template, &requestData, 0)) // context = index } else { @@ -118,23 +115,22 @@ func ShowTagTemplate(writer http.ResponseWriter, slug string, page int) error { return err } -func ShowIndexTemplate(writer http.ResponseWriter, page int) error { +func ShowIndexTemplate(w http.ResponseWriter, r *http.Request, page int) error { + // Read lock templates and global blog compiledTemplates.RLock() defer compiledTemplates.RUnlock() + methods.Blog.RLock() + defer methods.Blog.RUnlock() postIndex := int64(page - 1) if postIndex < 0 { postIndex = 0 } - blog, err := methods.GenerateBlog() - if err != nil { - return err - } - posts, err := database.RetrievePostsForIndex(blog.PostsPerPage, (blog.PostsPerPage * postIndex)) + posts, err := database.RetrievePostsForIndex(methods.Blog.PostsPerPage, (methods.Blog.PostsPerPage * postIndex)) if err != nil { return err } - requestData := structure.RequestData{Posts: posts, Blog: blog, CurrentIndexPage: page, CurrentTemplate: 0} // CurrentTemplate = index - _, err = writer.Write(executeHelper(compiledTemplates.m["index"], &requestData, 0)) // context = index + requestData := structure.RequestData{Posts: posts, Blog: methods.Blog, CurrentIndexPage: page, CurrentTemplate: 0, CurrentPath: r.URL.Path} // CurrentTemplate = index + _, err = w.Write(executeHelper(compiledTemplates.m["index"], &requestData, 0)) // context = index if requestData.PluginVMs != nil { // Put the lua state map back into the pool plugins.LuaPool.Put(requestData.PluginVMs) From 8ceb7a5eb38a890f4a1be49e3a6e1e5ee419bf05 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 6 May 2015 12:18:07 +0200 Subject: [PATCH 5/8] Format config.json --- config.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index cab0ff92..414a2617 100644 --- a/config.json +++ b/config.json @@ -1 +1,7 @@ -{"HttpHostAndPort":":8084","HttpsHostAndPort":":8085","HttpsUsage":"None","Url":"http://127.0.0.1:8084","HttpsUrl":"https://127.0.0.1:8085"} \ No newline at end of file +{ + "HttpHostAndPort":":8084", + "HttpsHostAndPort":":8085", + "HttpsUsage":"None", + "Url":"http://127.0.0.1:8084", + "HttpsUrl":"https://127.0.0.1:8085" +} \ No newline at end of file From 947712e65db793c4a111163b1b61873bb430276a Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 6 May 2015 13:14:12 +0200 Subject: [PATCH 6/8] Added positiveCeilingInt64 function by tomkwok --- templates/handlebars.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/templates/handlebars.go b/templates/handlebars.go index 976d78aa..d3cddb9a 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -185,7 +185,7 @@ func nextFunc(helper *structure.Helper, values *structure.RequestData) []byte { return []byte{} } } - maxPages := int64((float64(count) / float64(values.Blog.PostsPerPage)) + 0.5) + maxPages := positiveCeilingInt64(float64(count) / float64(values.Blog.PostsPerPage)) if int64(values.CurrentIndexPage) < maxPages { return []byte{1} } @@ -214,7 +214,11 @@ func pagesFunc(helper *structure.Helper, values *structure.RequestData) []byte { return []byte{} } } - maxPages := int64((float64(count) / float64(values.Blog.PostsPerPage)) + 0.5) + maxPages := positiveCeilingInt64(float64(count) / float64(values.Blog.PostsPerPage)) + // Output at least 1 (even if there are no posts in the database) + if maxPages == 0 { + maxPages = 1 + } return []byte(strconv.FormatInt(maxPages, 10)) } @@ -268,7 +272,7 @@ func page_urlFunc(helper *structure.Helper, values *structure.RequestData) []byt return []byte{} } } - maxPages := int64((float64(count) / float64(values.Blog.PostsPerPage)) + 0.5) + maxPages := positiveCeilingInt64(float64(count) / float64(values.Blog.PostsPerPage)) if int64(values.CurrentIndexPage) < maxPages { var buffer bytes.Buffer if values.CurrentTemplate == 3 { // author @@ -882,3 +886,11 @@ func evaluateEscape(value []byte, unescaped bool) []byte { } return []byte(html.EscapeString(string(value))) } + +func positiveCeilingInt64(input float64) int64 { + output := int64(input) + if (input - float64(output)) > 0 { + output++ + } + return output +} From 8544216bf27bf8539520bd06275cdc0991ced942 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 6 May 2015 15:45:32 +0200 Subject: [PATCH 7/8] Added log out link in admin interface. --- built-in/admin/admin-angular.js | 8 ++++---- built-in/admin/admin.css | 5 +++++ built-in/admin/post.html | 2 +- built-in/admin/settings.html | 2 +- server/admin.go | 2 +- templates/handlebars.go | 2 +- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/built-in/admin/admin-angular.js b/built-in/admin/admin-angular.js index d642e582..440df421 100644 --- a/built-in/admin/admin-angular.js +++ b/built-in/admin/admin-angular.js @@ -91,7 +91,7 @@ adminApp.factory('infiniteScrollFactory', function($http) { adminApp.controller('ContentCtrl', function ($scope, $http, $sce, $location, infiniteScrollFactory, sharingService){ //change the navbar according to controller - $scope.navbarHtml = $sce.trustAsHtml(''); + $scope.navbarHtml = $sce.trustAsHtml(''); $scope.infiniteScrollFactory = new infiniteScrollFactory('/admin/api/posts/'); $scope.openPost = function(postId) { $location.url('/edit/' + postId); @@ -112,7 +112,7 @@ adminApp.controller('ContentCtrl', function ($scope, $http, $sce, $location, inf adminApp.controller('SettingsCtrl', function ($scope, $http, $timeout, $sce, $location, sharingService){ //change the navbar according to controller - $scope.navbarHtml = $sce.trustAsHtml(''); + $scope.navbarHtml = $sce.trustAsHtml(''); $scope.shared = sharingService.shared; //variable to hold the field prefix $scope.prefix = ''; @@ -169,7 +169,7 @@ adminApp.controller('CreateCtrl', function ($scope, $http, $sce, $location, shar //create markdown converter var converter = new Showdown.converter(); //change the navbar according to controller - $scope.navbarHtml = $sce.trustAsHtml(''); + $scope.navbarHtml = $sce.trustAsHtml(''); $scope.shared = sharingService.shared; $scope.shared.post = {Title: 'New Post', Slug: '', Markdown: 'Write something!', IsPublished: false, Image: '', Tags: ''} $scope.change = function() { @@ -189,7 +189,7 @@ adminApp.controller('EditCtrl', function ($scope, $routeParams, $http, $sce, $lo //create markdown converter var converter = new Showdown.converter(); //change the navbar according to controller - $scope.navbarHtml = $sce.trustAsHtml(''); + $scope.navbarHtml = $sce.trustAsHtml(''); $scope.shared = sharingService.shared; $scope.shared.post = {} $scope.change = function() { diff --git a/built-in/admin/admin.css b/built-in/admin/admin.css index 9d9be536..cebe01da 100644 --- a/built-in/admin/admin.css +++ b/built-in/admin/admin.css @@ -65,6 +65,10 @@ img { margin-right: 15px; } +.logout { + color: #f04124 !important; +} + .post-content-row { cursor: pointer; } @@ -148,6 +152,7 @@ img { font-size: 16px; color: #ffffff; } + .navigation-item { padding-top: 10px; padding-bottom: 10px; diff --git a/built-in/admin/post.html b/built-in/admin/post.html index 112e4ea3..18f13201 100644 --- a/built-in/admin/post.html +++ b/built-in/admin/post.html @@ -64,7 +64,7 @@ diff --git a/built-in/admin/settings.html b/built-in/admin/settings.html index 124a8434..d6bb19d6 100644 --- a/built-in/admin/settings.html +++ b/built-in/admin/settings.html @@ -142,7 +142,7 @@

User {{shared.user.Name}}

diff --git a/server/admin.go b/server/admin.go index 2bf7c865..a16af771 100644 --- a/server/admin.go +++ b/server/admin.go @@ -146,7 +146,7 @@ func postRegistrationHandler(w http.ResponseWriter, r *http.Request, _ map[strin // Function to log out the user. Not used at the moment. func logoutHandler(w http.ResponseWriter, r *http.Request, _ map[string]string) { authentication.ClearSession(w) - http.Redirect(w, r, "/admin/", 302) + http.Redirect(w, r, "/admin/login/", 302) return } diff --git a/templates/handlebars.go b/templates/handlebars.go index d3cddb9a..8884b5b8 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -58,7 +58,7 @@ func currentFunc(helper *structure.Helper, values *structure.RequestData) []byte url = url + "/" } if values.CurrentPath == url { - return []byte("t") + return []byte{1} } } return []byte{} From a34b6fd0452a586b92927e9a51573f3a16719b56 Mon Sep 17 00:00:00 2001 From: Kai H Date: Wed, 6 May 2015 20:38:36 +0200 Subject: [PATCH 8/8] Fixed: Trailing slash redirection when using pages. --- server/pages.go | 12 ++++++++++-- templates/handlebars.go | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/server/pages.go b/server/pages.go index c077e8ce..dc4f890a 100644 --- a/server/pages.go +++ b/server/pages.go @@ -3,16 +3,24 @@ package server import ( "github.com/dimfeld/httptreemux" "github.com/kabukky/journey/filenames" + "github.com/kabukky/journey/helpers" "net/http" "path/filepath" + "strings" ) func pagesHandler(w http.ResponseWriter, r *http.Request, params map[string]string) { - http.ServeFile(w, r, filepath.Join(filenames.PagesFilepath, params["filepath"])) + path := filepath.Join(filenames.PagesFilepath, params["filepath"]) + // If the path points to a directory, add a trailing slash to the path (needed if the page loads relative assets). + if helpers.IsDirectory(path) && !strings.HasSuffix(r.RequestURI, "/") { + http.Redirect(w, r, r.RequestURI+"/", 301) + return + } + http.ServeFile(w, r, path) return } func InitializePages(router *httptreemux.TreeMux) { // For serving standalone projects or pages saved in in content/pages - router.GET("/pages/*filepath/", pagesHandler) + router.GET("/pages/*filepath", pagesHandler) } diff --git a/templates/handlebars.go b/templates/handlebars.go index 8884b5b8..90fbdc82 100644 --- a/templates/handlebars.go +++ b/templates/handlebars.go @@ -741,7 +741,7 @@ func atOddFunc(helper *structure.Helper, values *structure.RequestData) []byte { } func nameFunc(helper *structure.Helper, values *structure.RequestData) []byte { - // If tag (commented out the code for generating a link. Ghost doesn't seem to do that either. + // If tag (commented out the code for generating a link. Ghost doesn't seem to do that either). if values.CurrentHelperContext == 2 { // tag //var buffer bytes.Buffer //buffer.WriteString("