From f5485702a80bce7dc6f1a1c9c3a84ffe205c15a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Sun, 18 May 2025 15:56:29 +0300 Subject: [PATCH] add topics --- app.lua | 5 ++ apps/threads.lua | 7 +++ apps/topics.lua | 125 ++++++++++++++++++++++++++++++++++++++ apps/users.lua | 11 +--- migrations.lua | 12 ++++ models.lua | 4 ++ util.lua | 37 +++++++++++ views/topics/create.etlua | 6 ++ views/topics/edit.etlua | 12 ++++ views/topics/topic.etlua | 25 ++++++++ views/topics/topics.etlua | 16 +++++ 11 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 apps/threads.lua create mode 100644 apps/topics.lua create mode 100644 views/topics/create.etlua create mode 100644 views/topics/edit.etlua create mode 100644 views/topics/topic.etlua create mode 100644 views/topics/topics.etlua diff --git a/app.lua b/app.lua index c89cb0b..b47d82e 100644 --- a/app.lua +++ b/app.lua @@ -8,11 +8,16 @@ app.layout = require "views.base" local function inject_methods(req) req.avatar_url = util.get_user_avatar_url + req.ntob = function(_, v) + return util.ntob(v) + end end app:before_filter(inject_methods) app:include("apps.users", {path = "/user"}) +app:include("apps.topics", {path = "/topics"}) +app:include("apps.threads", {path = "/threads"}) app:get("/", function() return "Welcome to Lapis " .. require("lapis.version") diff --git a/apps/threads.lua b/apps/threads.lua new file mode 100644 index 0000000..93ef3fc --- /dev/null +++ b/apps/threads.lua @@ -0,0 +1,7 @@ +local app = require("lapis").Application() + +app:get("thread_create", "/create", function(self) + +end) + +return app diff --git a/apps/topics.lua b/apps/topics.lua new file mode 100644 index 0000000..5674285 --- /dev/null +++ b/apps/topics.lua @@ -0,0 +1,125 @@ +local app = require("lapis").Application() +local lapis_util = require("lapis.util") + +local db = require("lapis.db") +local constants = require("constants") + +local util = require("util") + +local models = require("models") +local Users = models.Users +local Avatars = models.Avatars +local Topics = models.Topics +local Threads = models.Threads + +local ThreadCreateError = { + OK = 0, + GUEST = 1, + LOGGED_OUT = 2, + TOPIC_LOCKED = 3, +} + +app:get("all_topics", "/", function(self) + self.topic_list = db.query("select * from topics limit 25;") + self.user = util.get_logged_in_user(self) or util.TransientUser + return {render = "topics.topics"} +end) + +app:get("topic_create", "/create", function(self) + local user = util.get_logged_in_user(self) or util.TransientUser + if not user:is_admin() then + return {status = 403} + end + + return {render = "topics.create"} +end) + +app:post("topic_create", "/create", function(self) + local user = util.get_logged_in_user(self) or util.TransientUser + if not user:is_admin() then + return {redirect_to = "all_topics"} + end + + local topic_name = lapis_util.trim(self.params.name) + local topic_description = self.params.description + local time = os.time() + local slug = lapis_util.slugify(topic_name) .. "-" .. time + + local topic = Topics:create({ + name = topic_name, + description = topic_description, + slug = slug, + }) + + return {redirect_to = self:url_for("all_topics")} +end) + +app:get("topic", "/:slug", function(self) + local topic = Topics:find({ + slug = self.params.slug + }) + if not topic then + return {status = 404} + end + self.topic = topic + self.threads_list = Threads:select(db.clause({ + topic_id = topic.id + })) + local user = util.get_logged_in_user_or_transient(self) + print(topic.is_locked, type(topic.is_locked)) + self.user = user + self.ThreadCreateError = ThreadCreateError + self.thread_create_error = ThreadCreateError.OK + if user:is_logged_in_guest() then + self.thread_create_error = ThreadCreateError.GUEST + elseif user:is_guest() then + self.thread_create_error = ThreadCreateError.LOGGED_OUT + elseif util.ntob(topic.is_locked) and not user:is_admin() then + self.thread_create_error = ThreadCreateError.TOPIC_LOCKED + end + + return {render = "topics.topic"} +end) + +app:get("topic_edit", "/:slug/edit", function(self) + local user = util.get_logged_in_user_or_transient(self) + if not user:is_admin() then + return {redirect_to = self:url_for("topic", {slug = self.params.slug})} + end + local topic = Topics:find({ + slug = self.params.slug + }) + if not topic then + return {redirect_to = self:url_for("all_topics")} + end + self.topic = topic + return {render = "topics.edit"} +end) + +app:post("topic_edit", "/:slug/edit", function(self) + local user = util.get_logged_in_user_or_transient(self) + if not user:is_admin() then + return {redirect_to = self:url_for("topic", {slug = self.params.slug})} + end + local topic = Topics:find({ + slug = self.params.slug + }) + if not topic then + return {redirect_to = self:url_for("all_topics")} + end + local name = self.params.name or topic.name + local description = self.params.description or topic.description + local is_locked = topic.is_locked + if self.params.is_locked ~= nil then + is_locked = util.form_bool_to_sqlite(self.params.is_locked) + end + + topic:update({ + name = name, + description = description, + is_locked = is_locked, + }) + return {redirect_to = self:url_for("topic", {slug = self.params.slug})} +end) + +return app diff --git a/apps/users.lua b/apps/users.lua index 5b35a8c..bbe7fd0 100644 --- a/apps/users.lua +++ b/apps/users.lua @@ -13,15 +13,6 @@ local Users = models.Users local Sessions = models.Sessions local Avatars = models.Avatars -local TransientUser = { - is_admin = function (self) - return false - end, - is_guest = function (self) - return true - end -} - local function authenticate_user(user, password) return bcrypt.verify(password, user.password_hash) end @@ -81,7 +72,7 @@ app:get("user", "/:username", function(self) end -- local me = validate_session(self.session.session_key) or TransientUser - local me = util.get_logged_in_user(self) or TransientUser + local me = util.get_logged_in_user(self) or util.TransientUser self.user = user self.me = me diff --git a/migrations.lua b/migrations.lua index 807272f..aba90d7 100644 --- a/migrations.lua +++ b/migrations.lua @@ -29,4 +29,16 @@ return { }) schema.add_column("users", "avatar_id", "REFERENCES avatars(id) ON DELETE SET NULL") end, + + [4] = function () + schema.add_column("topics", "description", types.text{default=""}) + + -- topic locked = no new threads can be created in the topic, but posts can be made in threads + -- thread locked = no new posts can be created in the thread, existing posts can not be edited + -- admins bypass both restrictions + schema.add_column("topics", "is_locked", "BOOLEAN DEFAULT FALSE") + schema.add_column("threads", "is_locked", "BOOLEAN DEFAULT FALSE") + -- will appear on top of non-stickied threads in topic view + schema.add_column("threads", "is_stickied", "BOOLEAN DEFAULT FALSE") + end, } diff --git a/models.lua b/models.lua index 9da8b62..1e7bfcf 100644 --- a/models.lua +++ b/models.lua @@ -12,6 +12,10 @@ function Users_mt:is_admin() return self.permission == constants.PermissionLevel.ADMIN end +function Users_mt:is_logged_in_guest() + return self:is_guest() and true +end + function Users_mt:is_default_avatar() return self.avatar_id == nil end diff --git a/util.lua b/util.lua index 261722d..cf693f4 100644 --- a/util.lua +++ b/util.lua @@ -5,6 +5,18 @@ local db = require("lapis.db") local Avatars = require("models").Avatars local Users = require("models").Users +util.TransientUser = { + is_admin = function (self) + return false + end, + is_guest = function (self) + return true + end, + is_logged_in_guest = function (self) + return false + end, +} + function util.get_user_avatar_url(req, user) if not user.avatar_id then return "/avatars/default.webp" @@ -57,4 +69,29 @@ function util.get_logged_in_user(req) return nil end +function util.get_logged_in_user_or_transient(req) + return util.get_logged_in_user(req) or util.TransientUser +end + +function util.ntob(v) + return v ~= 0 +end + +function util.bton(b) + return 1 and b or 0 +end + +function util.stob(s) + if s == "true" then + return true + end + if s == "false" then + return false + end +end + +function util.form_bool_to_sqlite(s) + return util.bton(util.stob(s)) +end + return util \ No newline at end of file diff --git a/views/topics/create.etlua b/views/topics/create.etlua new file mode 100644 index 0000000..8c657e6 --- /dev/null +++ b/views/topics/create.etlua @@ -0,0 +1,6 @@ +

Create topic

+
+
+
+ +
diff --git a/views/topics/edit.etlua b/views/topics/edit.etlua new file mode 100644 index 0000000..384245f --- /dev/null +++ b/views/topics/edit.etlua @@ -0,0 +1,12 @@ +

Editing topic <%= topic.name %>

+
+
+
+ +
+ +
+
"> + +
+Note: to preserve history, you cannot change the topic URL. \ No newline at end of file diff --git a/views/topics/topic.etlua b/views/topics/topic.etlua new file mode 100644 index 0000000..553dbcd --- /dev/null +++ b/views/topics/topic.etlua @@ -0,0 +1,25 @@ +

<%= topic.name %>

+

<%= topic.description %>

+<% if #threads_list == 0 then %> +

There are no threads in this topic.

+<% end %> + +<% if thread_create_error == ThreadCreateError.OK then %> +>New thread +<% elseif thread_create_error == ThreadCreateError.GUEST then %> +

Your account is still pending confirmation by an administrator. You are not able to create a new thread or post at this time.

+<% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %> +

Only logged in users can create threads. ">Sign up or ">log in to create a thread.

+<% else %> +

This topic is locked.

+<% end %> + +<% if user:is_admin() then %> +
+ ">Edit topic +
"> + +

<%= "This topic is " .. (ntob(topic.is_locked) and "" or "un") .. "locked." %>

+ "> +
+<% end %> diff --git a/views/topics/topics.etlua b/views/topics/topics.etlua new file mode 100644 index 0000000..18c3661 --- /dev/null +++ b/views/topics/topics.etlua @@ -0,0 +1,16 @@ +

Topics

+ +<% if #topic_list == 0 then %> +

There are no topics.

+<% else %> + +<% if user:is_admin() then %> +">Create new topic +<% end %>