aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/assets/js/admin/tabs/sites.js125
-rw-r--r--data/init.yaml75
-rw-r--r--src/guff/apis.cr48
-rw-r--r--src/guff/cli.cr9
-rw-r--r--src/guff/models/file.cr4
-rw-r--r--src/guff/models/post.cr3
-rw-r--r--src/guff/models/site.cr206
-rw-r--r--src/guff/views/pages/admin.cr1
-rw-r--r--src/views/panes/settings/sites.ecr67
9 files changed, 494 insertions, 44 deletions
diff --git a/data/assets/js/admin/tabs/sites.js b/data/assets/js/admin/tabs/sites.js
new file mode 100644
index 0000000..9ef9564
--- /dev/null
+++ b/data/assets/js/admin/tabs/sites.js
@@ -0,0 +1,125 @@
+jQuery(function($) {
+ "use strict";
+
+ var TEMPLATES = new LuigiTemplate.Cache({
+ site: [
+ "<a ",
+ "href='#' ",
+ "class='list-group-item %{css|h}' ",
+ "title='Edit site \"%{name|h}\".' ",
+ "data-site_id='%{site_id|h}' ",
+ "data-q='%{q|h}' ",
+ ">",
+ "%{name|h}",
+
+ "<span class='%{badge_css} badge pull-right'>",
+ "default",
+ "</span>",
+ "</a>",
+ ],
+
+ loading: [
+ "<span class='list-group-item disabled'>",
+ "<i class='fa fa-spinner fa-spin'></i>",
+ " ",
+ "Loading...",
+ "</span>",
+ ],
+
+ error: [
+ "<span class='list-group-item list-group-item-danger disabled'>",
+ "<i class='fa fa-exclamation-triangle'></i>",
+ " ",
+ "Error: %{error|h}",
+ "</span>",
+ ],
+ });
+
+ function filter() {
+ var qs = $('#sites-q').val().replace(/^\s+|\s+$/g, '').toLowerCase().split(/\s+/);
+
+ if (qs.length > 0) {
+ // hide all sites
+ $('#sites .list-group-item').addClass('hidden');
+
+ // show matching sites
+ $($.grep($('#sites .list-group-item'), function(el) {
+ var eq = $(el).data('q');
+
+ return ($.grep(qs, function(q) {
+ return eq.indexOf(q) !== -1;
+ }).length == qs.length);
+ })).removeClass('hidden');
+ } else {
+ // show all sites
+ $('#sites .list-group-item').removeClass('hidden');
+ }
+ }
+
+ function reload() {
+ var btn = $('#sites-reload'),
+ list = $('#sites');
+
+ // show loading
+ btn.toggleClass('disabled').find('.loading').toggleClass('hidden');
+ list.html(TEMPLATES.run('loading'));
+
+ send('site/get_sites').always(function() {
+ btn.toggleClass('disabled').find('.loading').toggleClass('hidden');
+ list.html('');
+ }).fail(function(r) {
+ var error = r.responseText;
+
+ try {
+ var data = $.parseJSON(r.responseText);
+ if (data.error)
+ error = data.error;
+ } catch (e) {}
+
+ list.html(TEMPLATES.run('error', {
+ error: error
+ }));
+ }).done(function(r) {
+ list.html($.map(r, function(row) {
+ var active = (row.is_active == '1');
+
+ return TEMPLATES.run('site', $.extend({}, row, {
+ q: [row.site_id, row.slug, row.name, row.body].join(' ').toLowerCase(),
+ badge_css: row.is_default ? '' : 'hidden',
+ }));
+ }).join(''));
+
+ // refresh filters
+ filter();
+ });
+
+ // stop event
+ return false;
+ }
+
+ $('#sites-q').on('search-update', function() {
+ filter();
+ });
+
+ $('#sites').on('click', 'a.list-group-item', function() {
+ var site_id = $(this).data('site_id');
+
+ // update highlight
+ $('#sites .active').removeClass('active');
+ $(this).addClass('active');
+
+ // TODO
+ // $('#site-edit-dialog').data('site_id', site_id).modal('show');
+ alert('TODO: edit site ' + site_id);
+
+ // stop event
+ return false;
+ });
+
+ $('#sites-reload').click(reload);
+
+ // load sites
+ $('#settings-tab-sites').on('show.bs.tab', function() {
+ reload();
+ });
+});
diff --git a/data/init.yaml b/data/init.yaml
index 34a0595..f9a8ac1 100644
--- a/data/init.yaml
+++ b/data/init.yaml
@@ -41,7 +41,7 @@ init_sql:
password TEXT NOT NULL DEFAULT '',
- is_active BOOLEAN NOT NULL DEFAULT false
+ is_active BOOLEAN NOT NULL DEFAULT 0
)
- |
@@ -88,7 +88,7 @@ init_sql:
-- is this a system theme?
-- note: system themes are located in system_dir/themes/<slug>/
- is_system BOOLEAN NOT NULL DEFAULT false
+ is_system BOOLEAN NOT NULL DEFAULT 0
)
- |
@@ -188,32 +188,67 @@ init_sql:
- |
CREATE TABLE sites (
- site_id INTEGER PRIMARY KEY,
+ site_id INTEGER PRIMARY KEY,
+
+ -- make sure slug is lowercase, does not begin with a dot, and
+ -- does not contain slashes
+ slug TEXT UNIQUE NOT NULL CHECK (
+ LENGTH(slug) > 0 AND
+ slug = LOWER(slug) AND
+ slug NOT LIKE '.%' AND
+ slug NOT LIKE '%/%'
+ ),
- -- make sure name does not begin with a dot or
+ -- make sure slug does not begin with a dot or
-- contain slashes
- name TEXT UNIQUE NOT NULL CHECK (
- LENGTH(name) > 0 AND
- name NOT LIKE '.%' AND
- name NOT LIKE '%/%'
- ),
+ name TEXT UNIQUE NOT NULL
+ CHECK (LENGTH(name) > 0),
+
+ -- site description
+ body TEXT NOT NULL DEFAULT '',
+
+ -- site description
+ lang TEXT NOT NULL DEFAULT 'en-US' CHECK (
+ -- e.g. 'en' or 'en-US'
+ (LENGTH(lang) = 2 OR LENGTH(lang) = 5) AND
+ (lang LIKE '__' OR lang LIKE '__-__')
+ ),
-- date that site was created
- created_at TIMESTAMP WITH TIME ZONE NOT NULL
- DEFAULT CURRENT_TIMESTAMP,
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL
+ DEFAULT CURRENT_TIMESTAMP,
-- theme for this site
- theme_id INTEGER NOT NULL
- REFERENCES themes(theme_id),
+ theme_id INTEGER NOT NULL
+ REFERENCES themes(theme_id),
+
+ -- disabled: too fucking complicated with "is_default"
+ -- is this site active
+ -- is_active BOOLEAN NOT NULL DEFAULT 0,
- is_active BOOLEAN NOT NULL DEFAULT false,
+ -- is this the default site
+ is_default BOOLEAN NOT NULL DEFAULT 0,
- is_default BOOLEAN NOT NULL DEFAULT false
+ -- use full body for rss and atom feeds
+ is_full_feed BOOLEAN NOT NULL DEFAULT 1
)
- |
- INSERT INTO sites(site_id, name, theme_id, is_active, is_default) VALUES
- (1, 'default', 1, 1, 1)
+ INSERT INTO sites (
+ site_id,
+ slug,
+ name,
+ body,
+ theme_id,
+ is_default
+ ) VALUES (
+ 1,
+ 'default',
+ 'Default',
+ 'True guff stuff.',
+ 1,
+ 1
+ )
- |
CREATE TABLE site_domains (
@@ -224,7 +259,11 @@ init_sql:
LENGTH(domain) > 0 AND
domain = LOWER(domain) AND
domain NOT LIKE '% %'
- )
+ ),
+
+ sort_order INTEGER NOT NULL,
+
+ PRIMARY KEY (site_id, domain)
)
- |
diff --git a/src/guff/apis.cr b/src/guff/apis.cr
index 8a84c1d..3754545 100644
--- a/src/guff/apis.cr
+++ b/src/guff/apis.cr
@@ -195,6 +195,54 @@ module Guff::APIs
def do_site_get_sites(params : HTTP::Params)
@context.models.site.get_sites
end
+
+ def do_site_get(params : HTTP::Params)
+ # get site_id
+ site_id = params["site_id"].to_i64
+
+ @context.models.site.get(site_id).reduce(
+ {} of String => String
+ ) do |r, kv|
+ r[kv[0]] = kv[1].to_s
+ r
+ end.merge({
+ "domains" => @context.models.site.domains(site_id),
+ } of String => Array(String))
+ end
+
+ def do_site_add(params : HTTP::Params)
+ {
+ site_id: @context.models.site.add(
+ name: params["name"],
+ slug: params["slug"],
+ lang: params["lang"],
+ body: params["body"],
+ is_full_feed: (params["is_full_feed"] == "t"),
+ theme_id: params["theme_id"].to_i64,
+ domains: Array(String).from_json(params["domains"]),
+ )
+ }
+ end
+
+ def do_site_set(params : HTTP::Params)
+ @context.models.site.set(
+ site_id: params["site_id"].to_i64,
+ name: params["name"]?,
+ slug: params["slug"]?,
+ lang: params["lang"]?,
+ body: params["body"]?,
+ is_full_feed: (params.includes?("is_full_key")) ? (params["is_full_feed"] == "t") : nil,
+ theme_id: params["theme_id"]? ? params["theme_id"].to_i64 : nil,
+ domains: params["domains"]? ? Array(String).from_json(params["domains"]) : nil,
+ )
+
+ nil
+ end
+
+ def do_site_set_default(params : HTTP::Params)
+ @context.models.site.set(params["site_id"].to_i64)
+ nil
+ end
end
module FileAPI
diff --git a/src/guff/cli.cr b/src/guff/cli.cr
index e5d8c6c..8ad6ce5 100644
--- a/src/guff/cli.cr
+++ b/src/guff/cli.cr
@@ -32,17 +32,14 @@ module Guff::CLI
end
# list of subdirectories to create in data directory
- DIRS = %w{files themes cache/theme-files}
+ DIRS = %w{themes files/default cache/theme-files}
def run
- # create data dir
- STDERR.puts "Initializing data directory"
- Dir.mkdir(@config.data_dir) unless Dir.exists?(@config.data_dir)
-
# create data subdirs
+ STDERR.puts "Initializing data directory"
DIRS.each do |dir|
abs_path = File.join(@config.data_dir, dir)
- Dir.mkdir_p(abs_path) unless Dir.exists?(abs_path)
+ Dir.mkdir_p(abs_path)
end
# create database
diff --git a/src/guff/models/file.cr b/src/guff/models/file.cr
index f55037c..51c8050 100644
--- a/src/guff/models/file.cr
+++ b/src/guff/models/file.cr
@@ -14,7 +14,7 @@ class Guff::Models::FileModel < Guff::Models::Model
base_path = File.expand_path(path, "/")
base_url = File.join(
"/guff/api/file/download",
- @context.models.site.get_name(site_id),
+ @context.models.site.get_slug(site_id),
base_path
)
@@ -102,7 +102,7 @@ class Guff::Models::FileModel < Guff::Models::Model
File.join(
@context.config.data_dir,
"files",
- @context.models.site.get_name(site_id),
+ @context.models.site.get_slug(site_id),
File.expand_path(path, "/")
)
end
diff --git a/src/guff/models/post.cr b/src/guff/models/post.cr
index 782cb31..5d79fb6 100644
--- a/src/guff/models/post.cr
+++ b/src/guff/models/post.cr
@@ -99,8 +99,7 @@ class Guff::Models::PostModel < Guff::Models::Model
LEFT JOIN projects z
ON (z.post_id = a.post_id)
- WHERE c.is_active
- AND %s
+ WHERE %s
ORDER BY COALESCE(a.posted_at, a.created_at) DESC
diff --git a/src/guff/models/site.cr b/src/guff/models/site.cr
index 4672fa4..4ea639e 100644
--- a/src/guff/models/site.cr
+++ b/src/guff/models/site.cr
@@ -10,8 +10,7 @@ class Guff::Models::SiteModel < Guff::Models::Model
JOIN sites b
ON (b.site_id = a.site_id)
- WHERE b.is_active
- AND a.domain = $1
+ WHERE a.domain = ?
UNION ALL
@@ -19,8 +18,7 @@ class Guff::Models::SiteModel < Guff::Models::Model
FROM sites
- WHERE is_active
- AND is_default
+ WHERE is_default
) a
LIMIT 1
@@ -31,28 +29,32 @@ class Guff::Models::SiteModel < Guff::Models::Model
FROM sites
- WHERE is_active
- AND is_default
+ WHERE is_default
",
get_sites: "
SELECT site_id,
+ slug,
name,
+ body,
+ lang,
theme_id,
- is_active,
+ is_full_feed,
is_default
FROM sites
ORDER BY LOWER(name)
-
",
get: "
SELECT a.site_id,
+ a.slug,
a.name,
- a.is_active,
+ a.body,
+ a.lang,
a.is_default,
+ a.is_full_feed,
a.theme_id,
b.theme_slug
@@ -60,16 +62,52 @@ class Guff::Models::SiteModel < Guff::Models::Model
JOIN themes b
ON (b.theme_id = a.theme_id)
+
WHERE a.site_id = ?
",
+ domains: "
+ SELECT domain
+ FROM site_domains
+ WHERE site_id = ?
+ ORDER BY sort_order
+ ",
+
get_name: "
SELECT name
- FROM sites a
+ FROM sites
WHERE site_id = ?
- AND is_active
+ ",
+
+ get_slug: "
+ SELECT slug
+ FROM sites
+ WHERE site_id = ?
+ ",
+
+ add: "
+ INSERT INTO sites (slug, name, theme_id) VALUES (?, ?, ?)
+ ",
+
+ set: "
+ UPDATE sites
+ SET %s
+ WHERE site_id = ?
+ ",
+
+ set_domains_clear: "
+ DELETE FROM site_domains WHERE site_id = ?
+ ",
+
+ set_domains_add: "
+ INSERT INTO site_domains(site_id, domain, sort_order) VALUES (?, ?, ?)
+ ",
+
+ set_default: "
+ UPDATE sites
+ SET is_default = (site_id = ?)
",
}
@@ -99,7 +137,153 @@ class Guff::Models::SiteModel < Guff::Models::Model
@context.dbs.ro.row(SQL[:get], [site_id.to_s]).not_nil!
end
+ def domains(site_id : Int64): Array(String)
+ r = [] of String
+
+ @context.dbs.ro.all(SQL[:domains], [site_id.to_s]) do |row|
+ r << row["domain"].as(String)
+ end
+
+ r
+ end
+
def get_name(site_id : Int64) : String
@context.dbs.ro.one(SQL[:get_name], [site_id.to_s]).as(String)
end
+
+ def get_slug(site_id : Int64) : String
+ @context.dbs.ro.one(SQL[:get_slug], [site_id.to_s]).to_s
+ end
+
+ def add(
+ slug : String? = nil,
+ name : String? = nil,
+ body : String? = nil,
+ lang : String? = nil,
+ is_full_feed : Bool? = nil,
+ theme_id : Int64? = nil,
+ domains : Array(String)? = nil,
+ ) : Int64
+ db = @context.dbs.rw
+ site_id = -1_i64
+
+ db.transaction do
+ db.query(SQL[:add], [slug, name, theme_id.to_s])
+ site_id = db.last_insert_row_id.to_i64
+
+ set(
+ site_id: site_id,
+ body: body,
+ lang: lang,
+ is_full_feed: is_full_feed,
+ domains: domains,
+ )
+
+ # create files directory
+ Dir.mkdir(File.join(
+ @context.config.data_dir,
+ "files",
+ File.expand_path(slug, "/")
+ ))
+ end
+
+ # return site_id
+ site_id
+ end
+
+ def set(
+ site_id : Int64,
+ slug : String? = nil,
+ name : String? = nil,
+ body : String? = nil,
+ lang : String? = nil,
+ is_full_feed : Bool? = nil,
+ theme_id : Int64? = nil,
+ domains : Array(String)? = nil,
+ )
+ sql = [] of String
+ args = [] of String
+
+ slug.try do |v|
+ sql << "slug = ?"
+ args << v
+ end
+
+ name.try do |v|
+ sql << "name = ?"
+ args << v
+ end
+
+ body.try do |v|
+ sql << "body = ?"
+ args << v
+ end
+
+ lang.try do |v|
+ sql << "lang = ?"
+ args << v
+ end
+
+ is_full_feed.try do |v|
+ sql << "is_full_feed = ?"
+ args << (v ? "1" : "0")
+ end
+
+ db = @context.dbs.rw
+
+ @context.dbs.rw.transaction do
+ if sql.size > 0
+ args << site_id.to_s
+ @context.dbs.rw.query(SQL[:set] % [sql.join(",")], args)
+ end
+
+ set_domains(site_id, domains)
+
+ slug.try do |slug|
+ # rename files directory
+ File.rename(File.join(
+ @context.config.data_dir,
+ "files",
+ get_slug(site_id)
+ ), File.join(
+ @context.config.data_dir,
+ "files",
+ File.expand_path(slug, "/"),
+ ))
+ end
+ end
+
+ nil
+ end
+
+ private def set_domains(
+ site_id : Int64,
+ domains : Array(String)? = nil,
+ )
+ domains.try do |domains|
+ db = @context.dbs.rw
+ id = site_id.to_s
+
+ db.transaction do
+ db.query(SQL[:set_domains_clear], [id])
+
+ domains.each_with_index do |domain, i|
+ db.query(SQL[:set_domains_add], [id, domain, i.to_s])
+ end
+ end
+ end
+
+ nil
+ end
+
+ def set_default(site_id : Int64)
+ db = @context.dbs.rw
+
+ db.transaction do
+ db.query(SQL[:set_default], [site_id.to_s])
+ get_default_id
+ end
+
+ nil
+ end
end
diff --git a/src/guff/views/pages/admin.cr b/src/guff/views/pages/admin.cr
index dd8537d..688e715 100644
--- a/src/guff/views/pages/admin.cr
+++ b/src/guff/views/pages/admin.cr
@@ -24,6 +24,7 @@ class Guff::Views::Pages::Admin < Guff::Views::HTMLView
assets/js/admin/tabs/files.js
assets/js/admin/tabs/import.js
assets/js/admin/tabs/users.js
+ assets/js/admin/tabs/sites.js
assets/js/admin/tabs/themes.js
assets/js/admin/dialogs/user-add.js
assets/js/admin/dialogs/user-edit.js
diff --git a/src/views/panes/settings/sites.ecr b/src/views/panes/settings/sites.ecr
index 774a023..d5cf91f 100644
--- a/src/views/panes/settings/sites.ecr
+++ b/src/views/panes/settings/sites.ecr
@@ -5,12 +5,69 @@
>
<div class='panel panel-default'>
<div class='panel-heading'>
+ <div class='btn-toolbar'>
+ <div class='btn-group btn-group-sm'>
+ <a
+ href='#'
+ class='btn btn-primary'
+ title='Create new site.'
+ data-toggle='modal'
+ data-target='#site-add-dialog'
+ >
+ <i class='fa fa-plus-circle'></i>
+ New Site
+ </a><!-- btn -->
+ </div><!-- btn-group -->
+
+ <div class='btn-group btn-group-sm pull-right'>
+ <a
+ href='#'
+ class='btn btn-default search-toggle'
+ title='Toggle search field.'
+ >
+ <i class='fa fa-fw fa-search'></i>
+ </a><!-- btn-->
+ </div><!-- btn-group -->
+
+ <div class='btn-group btn-group-sm pull-right'>
+ <a
+ href='#'
+ id='sites-reload'
+ class='btn btn-default'
+ title='Reload sites.'
+ >
+ <span class='loading'>
+ <i class='fa fa-fw fa-refresh'></i>
+ </span>
+
+ <span class='loading hidden'>
+ <i class='fa fa-fw fa-spinner fa-spin'></i>
+ </span>
+ </a><!-- #reload -->
+ </div><!-- btn-group -->
+ </div><!-- btn-toolbar -->
+ </div><!-- panel-heading -->
+
+ <div class='panel-heading hidden search-toggle'>
+ <div class='input-group input-group-sm'>
+ <span class='input-group-addon'>
+ <i class='fa fa-search'></i>
+ </span>
+
+ <input
+ type='text'
+ id='sites-q'
+ class='form-control'
+ title='Enter search terms'
+ />
+ </div><!-- input-group -->
</div><!-- panel-heading -->
- <div id='sites' class='panel-body'>
- <p>
- TODO: sites settings
- </p>
- </div><!-- panel-body -->
+ <div id='sites' class='list-group'>
+ <span class='list-group-item disabled'>
+ <i class='fa fa-spinner fa-spin'></i>
+ Loading...
+ </span><!-- list-group-item -->
+ </div><!-- list-group -->
</div><!-- panel -->
</div><!-- tab-pane -->