diff options
-rw-r--r-- | data/assets/js/admin/tabs/sites.js | 125 | ||||
-rw-r--r-- | data/init.yaml | 75 | ||||
-rw-r--r-- | src/guff/apis.cr | 48 | ||||
-rw-r--r-- | src/guff/cli.cr | 9 | ||||
-rw-r--r-- | src/guff/models/file.cr | 4 | ||||
-rw-r--r-- | src/guff/models/post.cr | 3 | ||||
-rw-r--r-- | src/guff/models/site.cr | 206 | ||||
-rw-r--r-- | src/guff/views/pages/admin.cr | 1 | ||||
-rw-r--r-- | src/views/panes/settings/sites.ecr | 67 |
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 --> |