├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── Tupfile ├── Tuprules.tup ├── app.moon ├── ci.sh ├── cmd ├── flow_methods.moon ├── routes.moon └── seed.moon ├── community ├── flows │ ├── bans.lua │ ├── bans.moon │ ├── blocks.lua │ ├── blocks.moon │ ├── bookmarks.lua │ ├── bookmarks.moon │ ├── browsing.lua │ ├── browsing.moon │ ├── categories.lua │ ├── categories.moon │ ├── category_groups.lua │ ├── category_groups.moon │ ├── members.lua │ ├── members.moon │ ├── moderators.lua │ ├── moderators.moon │ ├── pending_posts.lua │ ├── pending_posts.moon │ ├── posts.lua │ ├── posts.moon │ ├── reports.lua │ ├── reports.moon │ ├── subscriptions.lua │ ├── subscriptions.moon │ ├── topic_polls.lua │ ├── topic_polls.moon │ ├── topics.lua │ ├── topics.moon │ ├── votes.lua │ └── votes.moon ├── helpers │ ├── app.lua │ ├── app.moon │ ├── counters.lua │ ├── counters.moon │ ├── html.lua │ ├── html.moon │ ├── markdown.lua │ ├── markdown.moon │ ├── models.lua │ ├── models.moon │ ├── shapes.lua │ ├── shapes.moon │ ├── unicode.lua │ └── unicode.moon ├── limits.lua ├── limits.moon ├── migrations.lua ├── migrations.moon ├── model.lua ├── model.moon ├── models.lua ├── models.moon ├── models │ ├── activity_logs.lua │ ├── activity_logs.moon │ ├── bans.lua │ ├── bans.moon │ ├── blocks.lua │ ├── blocks.moon │ ├── bookmarks.lua │ ├── bookmarks.moon │ ├── categories.lua │ ├── categories.moon │ ├── category_group_categories.lua │ ├── category_group_categories.moon │ ├── category_groups.lua │ ├── category_groups.moon │ ├── category_members.lua │ ├── category_members.moon │ ├── category_post_logs.lua │ ├── category_post_logs.moon │ ├── category_tags.lua │ ├── category_tags.moon │ ├── community_users.lua │ ├── community_users.moon │ ├── moderation_log_objects.lua │ ├── moderation_log_objects.moon │ ├── moderation_logs.lua │ ├── moderation_logs.moon │ ├── moderators.lua │ ├── moderators.moon │ ├── pending_posts.lua │ ├── pending_posts.moon │ ├── poll_choices.lua │ ├── poll_choices.moon │ ├── poll_votes.lua │ ├── poll_votes.moon │ ├── post_edits.lua │ ├── post_edits.moon │ ├── post_reports.lua │ ├── post_reports.moon │ ├── posts.lua │ ├── posts.moon │ ├── posts_search.lua │ ├── posts_search.moon │ ├── subscriptions.lua │ ├── subscriptions.moon │ ├── topic_participants.lua │ ├── topic_participants.moon │ ├── topic_polls.lua │ ├── topic_polls.moon │ ├── topic_subscriptions.lua │ ├── topics.lua │ ├── topics.moon │ ├── user_category_last_seens.lua │ ├── user_category_last_seens.moon │ ├── user_topic_last_seens.lua │ ├── user_topic_last_seens.moon │ ├── virtual │ │ ├── user_users.lua │ │ └── user_users.moon │ ├── votes.lua │ ├── votes.moon │ ├── warnings.lua │ └── warnings.moon ├── schema.lua ├── schema.moon ├── spec │ ├── factory.lua │ └── factory.moon ├── version.lua └── version.moon ├── config.moon ├── lapis-community-dev-1.rockspec ├── lint_config.moon ├── mime.types ├── models.moon ├── models ├── Tupfile ├── community │ └── Tupfile └── users.moon ├── nginx.conf ├── schema.moon ├── schema.sql ├── spec ├── community_models.moon ├── counters_spec.moon ├── factory.moon ├── flow_helpers.moon ├── flows │ ├── bans_spec.moon │ ├── blocks_spec.moon │ ├── bookmarks_spec.moon │ ├── browsing_spec.moon │ ├── categories_spec.moon │ ├── category_groups_spec.moon │ ├── moderators_spec.moon │ ├── pending_posts_spec.moon │ ├── posting_spec.moon │ ├── reports_spec.moon │ ├── subscriptions_spec.moon │ ├── topic_polls_flow_spec.moon │ ├── topics_spec.moon │ └── votes_spec.moon ├── helpers.moon ├── helpers_spec.moon ├── models.moon └── models │ ├── activity_logs_spec.moon │ ├── bookmarks_spec.moon │ ├── categories_spec.moon │ ├── category_groups_spec.moon │ ├── category_post_logs_spec.moon │ ├── category_tags_spec.moon │ ├── community_users_spec.moon │ ├── moderation_logs_spec.moon │ ├── moderators_spec.moon │ ├── pending_posts_spec.moon │ ├── posts_search_spec.moon │ ├── posts_spec.moon │ ├── topic_participants_spec.moon │ ├── topic_polls_spec.moon │ ├── topics_spec.moon │ ├── votes_spec.moon │ └── warnings.moon ├── tags ├── views ├── Tupfile ├── category.moon ├── category_accept_member.moon ├── category_accept_moderator.moon ├── category_members.moon ├── category_moderators.moon ├── category_new_member.moon ├── category_new_moderator.moon ├── delete_post.moon ├── edit_category.moon ├── edit_post.moon ├── index.moon ├── layout.moon ├── lock_topic.moon ├── login.moon ├── new_category.moon ├── new_post.moon ├── new_topic.moon ├── post.moon ├── register.moon ├── reply_post.moon ├── stick_topic.moon ├── topic.moon └── user.moon └── widgets ├── Tupfile ├── base.moon └── posts.moon /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [leafo] 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "test" 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | 8 | env: 9 | PGUSER: postgres 10 | PGPASSWORD: postgres 11 | PGHOST: 127.0.0.1 12 | 13 | services: 14 | postgres: 15 | image: postgres:12 16 | env: 17 | POSTGRES_PASSWORD: postgres 18 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 19 | ports: 20 | - 5432:5432 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | - uses: leafo/gh-actions-lua@master 25 | with: 26 | luaVersion: "luajit-openresty" 27 | buildCache: false 28 | 29 | - uses: leafo/gh-actions-luarocks@master 30 | 31 | - name: build 32 | run: | 33 | luarocks install busted 34 | luarocks install moonscript 35 | luarocks install cmark # optional dep 36 | luarocks make 37 | moonc schema.moon 38 | moonc config.moon 39 | moonc community 40 | 41 | - name: setup db 42 | run: | 43 | psql -c 'create database community_test' 44 | echo "return 'test'" > lapis_environment.lua 45 | LAPIS_SHOW_QUERIES=1 lua -e 'require("schema").make_schema()' 46 | 47 | - name: test 48 | run: | 49 | busted -o utfTerminal 50 | 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.lua 2 | !community/** 3 | logs/ 4 | nginx.conf.compiled 5 | .tup 6 | ##### TUP GITIGNORE ##### 7 | ##### Lines below automatically generated by Tup. 8 | ##### Do not edit. 9 | .tup 10 | /.gitignore 11 | /app.lua 12 | /config.lua 13 | /lint_config.lua 14 | /models.lua 15 | /schema.lua 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/leafo/lapis-archlinux-itchio:latest 2 | MAINTAINER leaf corcoran 3 | 4 | WORKDIR /site/lapis-community 5 | ADD . . 6 | ENTRYPOINT ./ci.sh 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: clean_test test local count build seed schema.sql 3 | 4 | clean_test: build 5 | -dropdb -U postgres community_test 6 | createdb -U postgres community_test 7 | LAPIS_SHOW_QUERIES=1 LAPIS_ENVIRONMENT=test lua5.1 -e 'require("schema").make_schema()' 8 | make schema.sql 9 | 10 | tags:: 11 | moon-tags $$(git ls-files community/ | grep -v '/spec/' | grep '\.moon$$') > $@ 12 | 13 | clean_dev: 14 | -dropdb -U postgres community 15 | createdb -U postgres community 16 | LAPIS_SHOW_QUERIES=1 LAPIS_ENVIRONMENT=development lua5.1 -e 'require("schema").make_schema()' 17 | 18 | seed: 19 | LAPIS_SHOW_QUERIES=1 moon cmd/seed.moon 20 | 21 | test: 22 | busted 23 | 24 | lint: 25 | moonc -l community/ 26 | moonc -l spec/ 27 | # moonc -l views/ 28 | moonc -l app.moon 29 | 30 | count: 31 | wc -l $$(git ls-files | grep 'scss$$\|moon$$\|coffee$$\|md$$\|conf$$') | sort -n | tail 32 | 33 | build: 34 | moonc community 35 | tup upd 36 | 37 | local: build 38 | luarocks --lua-version=5.1 make --local lapis-community-dev-1.rockspec 39 | 40 | annotate_models: clean_dev 41 | lapis annotate $$(find community/models -type f | grep -v /virtual/ | grep moon$$) 42 | 43 | # update the schema.sql from schema in dev db 44 | schema.sql: 45 | pg_dump -s -U postgres community_test > schema.sql 46 | pg_dump -a -t lapis_migrations -U postgres community_test >> schema.sql 47 | 48 | new_migration:: 49 | lapis generate migration --migrations-module community.migrations --counter increment 50 | -------------------------------------------------------------------------------- /Tupfile: -------------------------------------------------------------------------------- 1 | include_rules 2 | -------------------------------------------------------------------------------- /Tuprules.tup: -------------------------------------------------------------------------------- 1 | .gitignore 2 | : foreach *.moon |> moonc %f |> %B.lua 3 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -o pipefail 5 | set -o xtrace 6 | 7 | 8 | luarocks --lua-version=5.1 install busted 9 | luarocks --lua-version=5.1 install https://raw.githubusercontent.com/leafo/lapis/master/lapis-dev-1.rockspec 10 | luarocks --lua-version=5.1 install moonscript 11 | luarocks --lua-version=5.1 install date 12 | luarocks --lua-version=5.1 install web_sanitize 13 | luarocks --lua-version=5.1 install tableshape 14 | luarocks --lua-version=5.1 install cmark 15 | 16 | luarocks --lua-version=5.1 make 17 | 18 | # start postgres 19 | echo "fsync = off" >> /var/lib/postgres/data/postgresql.conf 20 | echo "synchronous_commit = off" >> /var/lib/postgres/data/postgresql.conf 21 | echo "full_page_writes = off" >> /var/lib/postgres/data/postgresql.conf 22 | su postgres -c '/usr/bin/pg_ctl -s -D /var/lib/postgres/data start -w -t 120' 23 | 24 | echo "return 'test'" > lapis_environment.lua 25 | 26 | moonc schema.moon 27 | moonc config.moon 28 | moonc community 29 | createdb -U postgres community_test 30 | LAPIS_SHOW_QUERIES=1 /usr/local/openresty/luajit/bin/luajit -e 'require("schema").make_schema()' 31 | 32 | cat $(which busted) | sed 's/\/usr\/bin\/lua5\.1/\/usr\/local\/openresty\/luajit\/bin\/luajit/' > busted 33 | chmod +x busted 34 | ./busted -o utfTerminal 35 | -------------------------------------------------------------------------------- /cmd/flow_methods.moon: -------------------------------------------------------------------------------- 1 | 2 | exec = (cmd) -> 3 | f = io.popen cmd 4 | with f\read("*all")\gsub "%s*$", "" 5 | f\close! 6 | 7 | for mod in exec("ls community/flows/*.moon")\gmatch "([%w_]+)%.moon" 8 | flow = require "community.flows.#{mod}" 9 | print flow.__name 10 | 11 | methods = [k for k,v in pairs flow.__base when type(v) == "function"] 12 | table.sort methods 13 | 14 | for m in *methods 15 | print " #{m}" 16 | 17 | print! 18 | 19 | 20 | -------------------------------------------------------------------------------- /cmd/routes.moon: -------------------------------------------------------------------------------- 1 | moon = require "moon" 2 | app = require"app"! 3 | 4 | import columnize from require "lapis.cmd.util" 5 | 6 | tuples = [{k,v} for k,v in pairs app.router.named_routes] 7 | table.sort tuples, (a,b) -> 8 | a[1] < b[1] 9 | 10 | print columnize tuples, 0, 4, false 11 | 12 | -------------------------------------------------------------------------------- /cmd/seed.moon: -------------------------------------------------------------------------------- 1 | 2 | 3 | factory = require "spec.factory" 4 | 5 | words = [word for word in io.open("/usr/share/dict/american-english")\lines!] 6 | 7 | -- average = 1 8 | random_normal = -> 9 | _random = math.random 10 | (_random! + _random! + _random! + _random! + _random! + _random! + _random! + _random! + _random! + _random! + _random! + _random!) / 6 11 | 12 | pick_one = (...) -> 13 | num = select "#", ... 14 | (select math.random(num), ...) 15 | 16 | sentence = (num_words=5) -> 17 | table.concat [words[math.random 1, #words] for i=1,num_words], " " 18 | 19 | leafo = factory.Users username: "leafo", community_user: true 20 | lee = factory.Users username: "lee", community_user: true 21 | adam = factory.Users username: "adam", community_user: true 22 | fart = factory.Users username: "fart", community_user: true 23 | 24 | rand_user = -> pick_one leafo, lee, adam, fart 25 | 26 | cat1 = factory.Categories { 27 | user_id: leafo.id 28 | title: "Leafo's category" 29 | membership_type: "public" 30 | } 31 | 32 | cat2 = factory.Categories { 33 | user_id: leafo.id 34 | title: "Lee's zone" 35 | membership_type: "members_only" 36 | voting_type: "up" 37 | } 38 | 39 | add_posts = (topic, parent_post) -> 40 | base_count = if parent_post 41 | 5 42 | else 43 | 22 44 | 45 | num_posts = math.floor base_count * random_normal! 46 | 47 | for i=1,num_posts 48 | poster = rand_user! 49 | post = factory.Posts { 50 | user_id: poster.id 51 | topic_id: topic.id 52 | body: sentence math.random 8, 10 53 | parent_post: parent_post 54 | } 55 | 56 | print 57 | k = math.abs(random_normal! - 1) 58 | if k > 0.1 * post.depth 59 | add_posts topic, post 60 | 61 | for i=1,4 -- 77 62 | topic_poster = rand_user! 63 | topic = factory.Topics { 64 | category_id: cat1.id 65 | user_id: topic_poster.id 66 | title: sentence math.random 2, 5 67 | } 68 | 69 | add_posts topic 70 | 71 | for i=1,1 72 | topic_poster = rand_user! 73 | topic = factory.Topics { 74 | category_id: cat2.id 75 | user_id: topic_poster.id 76 | title: sentence math.random 2, 5 77 | } 78 | 79 | add_posts topic 80 | 81 | import Categories, Topics, CommunityUsers from require "community.models" 82 | 83 | Topics\recount! 84 | Categories\recount! 85 | CommunityUsers\recount! 86 | 87 | for c in *Categories\select! 88 | c\refresh_last_topic! 89 | 90 | for t in *Topics\select! 91 | t\refresh_last_post! 92 | -------------------------------------------------------------------------------- /community/flows/blocks.lua: -------------------------------------------------------------------------------- 1 | local Flow 2 | Flow = require("lapis.flow").Flow 3 | local Users 4 | Users = require("models").Users 5 | local Blocks 6 | Blocks = require("community.models").Blocks 7 | local assert_error 8 | assert_error = require("lapis.application").assert_error 9 | local assert_page, require_current_user 10 | do 11 | local _obj_0 = require("community.helpers.app") 12 | assert_page, require_current_user = _obj_0.assert_page, _obj_0.require_current_user 13 | end 14 | local preload 15 | preload = require("lapis.db.model").preload 16 | local assert_valid 17 | assert_valid = require("lapis.validate").assert_valid 18 | local types = require("lapis.validate.types") 19 | local BlocksFlow 20 | do 21 | local _class_0 22 | local _parent_0 = Flow 23 | local _base_0 = { 24 | expose_assigns = true, 25 | show_blocks = function(self) 26 | assert_page(self) 27 | self.pager = Blocks:paginated("\n where blocking_user_id = ?\n order by created_at desc\n ", self.current_user.id, { 28 | per_page = 40, 29 | prepare_results = function(blocks) 30 | preload(blocks, "blocked_user") 31 | return blocks 32 | end 33 | }) 34 | self.blocks = self.pager:get_page(self.page) 35 | return self.blocks 36 | end, 37 | load_blocked_user = function(self) 38 | if self.blocked then 39 | return 40 | end 41 | local params = assert_valid(self.params, types.params_shape({ 42 | { 43 | "blocked_user_id", 44 | types.db_id 45 | } 46 | })) 47 | self.blocked = assert_error(Users:find(params.blocked_user_id), "invalid user") 48 | return assert_error(self.blocked.id ~= self.current_user.id, "you can not block yourself") 49 | end, 50 | block_user = function(self) 51 | self:load_blocked_user() 52 | Blocks:create({ 53 | blocking_user_id = self.current_user.id, 54 | blocked_user_id = self.blocked.id 55 | }) 56 | return true 57 | end, 58 | unblock_user = function(self) 59 | self:load_blocked_user() 60 | local block = Blocks:find({ 61 | blocking_user_id = self.current_user.id, 62 | blocked_user_id = self.blocked.id 63 | }) 64 | return block and block:delete() 65 | end 66 | } 67 | _base_0.__index = _base_0 68 | setmetatable(_base_0, _parent_0.__base) 69 | _class_0 = setmetatable({ 70 | __init = function(self, req) 71 | _class_0.__parent.__init(self, req) 72 | return assert(self.current_user, "missing current user for blocks flow") 73 | end, 74 | __base = _base_0, 75 | __name = "BlocksFlow", 76 | __parent = _parent_0 77 | }, { 78 | __index = function(cls, name) 79 | local val = rawget(_base_0, name) 80 | if val == nil then 81 | local parent = rawget(cls, "__parent") 82 | if parent then 83 | return parent[name] 84 | end 85 | else 86 | return val 87 | end 88 | end, 89 | __call = function(cls, ...) 90 | local _self_0 = setmetatable({}, _base_0) 91 | cls.__init(_self_0, ...) 92 | return _self_0 93 | end 94 | }) 95 | _base_0.__class = _class_0 96 | if _parent_0.__inherited then 97 | _parent_0.__inherited(_parent_0, _class_0) 98 | end 99 | BlocksFlow = _class_0 100 | return _class_0 101 | end 102 | -------------------------------------------------------------------------------- /community/flows/blocks.moon: -------------------------------------------------------------------------------- 1 | 2 | import Flow from require "lapis.flow" 3 | 4 | import Users from require "models" 5 | import Blocks from require "community.models" 6 | 7 | import assert_error from require "lapis.application" 8 | import assert_page, require_current_user from require "community.helpers.app" 9 | 10 | import preload from require "lapis.db.model" 11 | 12 | import assert_valid from require "lapis.validate" 13 | types = require "lapis.validate.types" 14 | 15 | class BlocksFlow extends Flow 16 | expose_assigns: true 17 | 18 | new: (req) => 19 | super req 20 | assert @current_user, "missing current user for blocks flow" 21 | 22 | show_blocks: => 23 | assert_page @ 24 | 25 | @pager = Blocks\paginated " 26 | where blocking_user_id = ? 27 | order by created_at desc 28 | ", @current_user.id, { 29 | per_page: 40 30 | prepare_results: (blocks) -> 31 | preload blocks, "blocked_user" 32 | blocks 33 | } 34 | 35 | @blocks = @pager\get_page @page 36 | @blocks 37 | 38 | load_blocked_user: => 39 | return if @blocked 40 | 41 | params = assert_valid @params, types.params_shape { 42 | {"blocked_user_id", types.db_id } 43 | } 44 | 45 | @blocked = assert_error Users\find(params.blocked_user_id), "invalid user" 46 | assert_error @blocked.id != @current_user.id, "you can not block yourself" 47 | 48 | block_user: => 49 | @load_blocked_user! 50 | Blocks\create { 51 | blocking_user_id: @current_user.id 52 | blocked_user_id: @blocked.id 53 | } 54 | 55 | true 56 | 57 | unblock_user: => 58 | @load_blocked_user! 59 | block = Blocks\find { 60 | blocking_user_id: @current_user.id 61 | blocked_user_id: @blocked.id 62 | } 63 | 64 | block and block\delete! 65 | -------------------------------------------------------------------------------- /community/flows/bookmarks.moon: -------------------------------------------------------------------------------- 1 | 2 | import Flow from require "lapis.flow" 3 | 4 | db = require "lapis.db" 5 | 6 | import assert_error from require "lapis.application" 7 | import assert_valid from require "lapis.validate" 8 | 9 | import Users from require "models" 10 | import Bookmarks from require "community.models" 11 | 12 | import require_current_user, assert_page from require "community.helpers.app" 13 | 14 | import preload from require "lapis.db.model" 15 | 16 | types = require "lapis.validate.types" 17 | 18 | class BookmarksFlow extends Flow 19 | expose_assigns: true 20 | 21 | new: (req) => 22 | super req 23 | assert @current_user, "missing current user for bookmarks flow" 24 | 25 | load_object: => 26 | return if @object 27 | 28 | params = assert_valid @params, types.params_shape { 29 | {"object_id", types.db_id} 30 | {"object_type", types.db_enum Bookmarks.object_types} 31 | } 32 | 33 | model = Bookmarks\model_for_object_type params.object_type 34 | @object = model\find params.object_id 35 | 36 | assert_error @object, "invalid bookmark object" 37 | 38 | @bookmark = Bookmarks\get @object, @current_user 39 | 40 | -- shows the current user's bookmarks across the entire community 41 | show_topic_bookmarks: require_current_user => 42 | BrowsingFlow = require "community.flows.browsing" 43 | 44 | -- TODO: this query can be bad 45 | -- TODO: not all topics have last post 46 | import Topics, Categories from require "community.models" 47 | 48 | @pager = Topics\paginated " 49 | where id in ( 50 | select object_id from #{db.escape_identifier Bookmarks\table_name!} 51 | where user_id = ? and object_type = ? 52 | ) 53 | and not deleted 54 | order by last_post_id desc 55 | ", @current_user.id, Bookmarks.object_types.topic, { 56 | per_page: 50 57 | prepare_results: (topics) -> 58 | preload topics, "category" 59 | Topics\preload_bans topics, @current_user 60 | 61 | categories = [t\get_category! for t in *topics] 62 | Categories\preload_bans categories, @current_user 63 | preload categories, "tags" 64 | BrowsingFlow(@)\preload_topics topics 65 | topics 66 | } 67 | 68 | assert_page @ 69 | @topics = @pager\get_page @page 70 | 71 | save_bookmark: => 72 | @load_object! 73 | assert_error @object\allowed_to_view(@current_user, @_req), "invalid object" 74 | Bookmarks\save @object, @current_user 75 | 76 | remove_bookmark: => 77 | @load_object! 78 | Bookmarks\remove @object, @current_user 79 | 80 | -------------------------------------------------------------------------------- /community/flows/category_groups.moon: -------------------------------------------------------------------------------- 1 | import Flow from require "lapis.flow" 2 | 3 | db = require "lapis.db" 4 | 5 | import assert_valid, with_params from require "lapis.validate" 6 | import assert_error from require "lapis.application" 7 | import require_current_user, assert_page from require "community.helpers.app" 8 | import filter_update from require "community.helpers.models" 9 | 10 | import CategoryGroups from require "community.models" 11 | 12 | limits = require "community.limits" 13 | shapes = require "community.helpers.shapes" 14 | types = require "lapis.validate.types" 15 | 16 | class CategoryGroupsFlow extends Flow 17 | expose_assigns: true 18 | 19 | bans_flow: => 20 | @load_category_group! 21 | BansFlow = require "community.flows.bans" 22 | BansFlow @, @category_group 23 | 24 | load_category_group: => 25 | return if @category_group 26 | 27 | assert_valid @params, types.params_shape { 28 | {"category_group_id", types.db_id} 29 | } 30 | 31 | @category_group = CategoryGroups\find @params.category_group_id 32 | assert_error @category_group, "invalid group" 33 | 34 | validate_params: with_params { 35 | {"category_group", types.params_shape { 36 | {"title", shapes.db_nullable types.limited_text limits.MAX_TITLE_LEN } 37 | {"description", shapes.db_nullable types.limited_text limits.MAX_BODY_LEN } 38 | {"rules", shapes.db_nullable types.limited_text limits.MAX_BODY_LEN } 39 | }} 40 | }, (params) => params.category_group 41 | 42 | new_category_group: require_current_user => 43 | create_params = @validate_params! 44 | create_params.user_id = @current_user.id 45 | @category_group = CategoryGroups\create create_params 46 | true 47 | 48 | edit_category_group: require_current_user => 49 | @load_category_group! 50 | assert_error @category_group\allowed_to_edit(@current_user), 51 | "invalid category group" 52 | 53 | update_params = @validate_params! 54 | update_params = filter_update @category_group, update_params 55 | @category_group\update update_params 56 | true 57 | 58 | moderators_flow: => 59 | @load_category_group! 60 | ModeratorsFlow = require "community.flows.moderators" 61 | ModeratorsFlow @, @category_group 62 | 63 | show_categories: => 64 | @load_category_group! 65 | assert_page @ 66 | 67 | @pager = @category_group\get_categories_paginated! 68 | @categories = @pager\get_page @page 69 | @categories 70 | 71 | 72 | -------------------------------------------------------------------------------- /community/flows/members.moon: -------------------------------------------------------------------------------- 1 | 2 | import Flow from require "lapis.flow" 3 | 4 | db = require "lapis.db" 5 | import with_params from require "lapis.validate" 6 | import assert_error from require "lapis.application" 7 | import assert_page, require_current_user from require "community.helpers.app" 8 | 9 | import Users from require "models" 10 | import CategoryMembers from require "community.models" 11 | 12 | import preload from require "lapis.db.model" 13 | 14 | types = require "lapis.validate.types" 15 | 16 | class MembersFlow extends Flow 17 | expose_assigns: true 18 | 19 | new: (req) => 20 | super req 21 | assert @category, "can't create a members flow without a category on the request object" 22 | 23 | load_user: with_params { 24 | {"user_id", types.empty + types.db_id} 25 | {"username", types.empty + types.limited_text 256} 26 | }, (params) => 27 | user = if params.user_id 28 | Users\find params.user_id 29 | elseif params.username 30 | Users\find username: params.username 31 | 32 | assert_error user, "invalid user" 33 | assert_error @current_user.id != user.id, "can't add self" 34 | 35 | @user = user 36 | @member = @category\find_member @user 37 | 38 | show_members: => 39 | assert_page @ 40 | 41 | @pager = CategoryMembers\paginated [[ 42 | where category_id = ? 43 | order by created_at desc, user_id desc 44 | ]], @category.id, per_page: 20, prepare_results: (members) -> 45 | preload members, "user" 46 | members 47 | 48 | @members = @pager\get_page @page 49 | @members 50 | 51 | add_member: require_current_user => 52 | assert_error @category\allowed_to_edit_members(@current_user), "invalid category" 53 | @load_user! 54 | assert_error not @member, "already a member" 55 | CategoryMembers\create category_id: @category.id, user_id: @user.id 56 | true 57 | 58 | remove_member: require_current_user => 59 | assert_error @category\allowed_to_edit_members(@current_user), "invalid category" 60 | @load_user! 61 | assert_error @member, "user is not member" 62 | @member\delete! 63 | true 64 | 65 | accept_member: require_current_user => 66 | member = CategoryMembers\find { 67 | category_id: @category.id 68 | user_id: @current_user.id 69 | accepted: false 70 | } 71 | assert_error member, "no pending membership" 72 | member\update accepted: true 73 | true 74 | -------------------------------------------------------------------------------- /community/flows/moderators.moon: -------------------------------------------------------------------------------- 1 | 2 | import Flow from require "lapis.flow" 3 | 4 | db = require "lapis.db" 5 | 6 | import assert_valid from require "lapis.validate" 7 | import assert_error from require "lapis.application" 8 | import assert_page, require_current_user from require "community.helpers.app" 9 | 10 | types = require "lapis.validate.types" 11 | 12 | import Users from require "models" 13 | 14 | import Moderators from require "community.models" 15 | 16 | import preload from require "lapis.db.model" 17 | 18 | class ModeratorsFlow extends Flow 19 | expose_assigns: true 20 | 21 | new: (req, @object) => 22 | super req 23 | 24 | load_object: => 25 | return if @object 26 | 27 | params = assert_valid @params, types.params_shape { 28 | {"object_id", types.db_id} 29 | {"object_type", types.db_enum Moderators.object_types} 30 | } 31 | 32 | model = Moderators\model_for_object_type params.object_type 33 | @object = model\find params.object_id 34 | assert_error @object, "invalid moderator object" 35 | 36 | load_user: (allow_self) => 37 | @load_object! 38 | 39 | return if @user 40 | 41 | @user = if @params.user_id 42 | user_id = assert_error types.db_id\describe("user_id")\transform @params.user_id 43 | Users\find user_id 44 | elseif @params.username 45 | username = assert_error types.limited_text(255)\describe("username")\transform @params.username 46 | Users\find { :username } 47 | 48 | assert_error @user, "invalid user" 49 | 50 | unless allow_self 51 | assert_error not @current_user or @current_user.id != @user.id, 52 | "you can't chose yourself" 53 | 54 | @moderator = Moderators\find_for_object_user @object, @user 55 | 56 | add_moderator: require_current_user => 57 | @load_user! 58 | 59 | assert_error @object\allowed_to_edit_moderators(@current_user), 60 | "invalid moderatable object" 61 | 62 | assert_error not @object\allowed_to_moderate(@user, true), 63 | "already moderator" 64 | 65 | Moderators\create { 66 | user_id: @user.id 67 | object: @object 68 | } 69 | 70 | remove_moderator: require_current_user => 71 | @load_user true 72 | 73 | -- you can remove yourself 74 | unless @moderator and @moderator.user_id == @current_user.id 75 | assert_error @object\allowed_to_edit_moderators(@current_user), 76 | "invalid moderatable object" 77 | 78 | assert_error @moderator, "not a moderator" 79 | 80 | @moderator\delete! 81 | 82 | show_moderators: => 83 | @load_object! 84 | assert_page @ 85 | 86 | @pager = Moderators\paginated " 87 | where object_type = ? and object_id = ? 88 | order by created_at desc, user_id asc 89 | ", Moderators\object_type_for_object(@object), @object.id, { 90 | per_page: 20 91 | prepare_results: (moderators) -> 92 | preload moderators, "user" 93 | moderators 94 | } 95 | 96 | @moderators = @pager\get_page @page 97 | @moderators 98 | 99 | get_pending_moderator: => 100 | unless @pending_moderator 101 | @load_object! 102 | mod = Moderators\find_for_object_user @object, @current_user 103 | @pending_moderator = mod and not mod.accepted and mod 104 | 105 | @pending_moderator 106 | 107 | accept_moderator_position: require_current_user => 108 | mod = assert_error @get_pending_moderator!, "invalid moderator" 109 | mod\update accepted: true 110 | true 111 | 112 | 113 | -------------------------------------------------------------------------------- /community/flows/pending_posts.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Flow 3 | Flow = require("lapis.flow").Flow 4 | local PendingPosts, ActivityLogs, ModerationLogs 5 | do 6 | local _obj_0 = require("community.models") 7 | PendingPosts, ActivityLogs, ModerationLogs = _obj_0.PendingPosts, _obj_0.ActivityLogs, _obj_0.ModerationLogs 8 | end 9 | do 10 | local _class_0 11 | local _parent_0 = Flow 12 | local _base_0 = { 13 | delete_pending_post = function(self, pending_post) 14 | if pending_post:delete() then 15 | ActivityLogs:create({ 16 | user_id = self.current_user.id, 17 | object = pending_post, 18 | action = "delete" 19 | }) 20 | return true 21 | end 22 | end, 23 | promote_pending_post = function(self, pending_post) 24 | local post, err = pending_post:promote() 25 | if not (post) then 26 | return nil, err 27 | end 28 | ActivityLogs:create({ 29 | user_id = self.current_user.id, 30 | object = pending_post, 31 | action = "promote", 32 | data = { 33 | post_id = post.id 34 | } 35 | }) 36 | return post 37 | end 38 | } 39 | _base_0.__index = _base_0 40 | setmetatable(_base_0, _parent_0.__base) 41 | _class_0 = setmetatable({ 42 | __init = function(self, ...) 43 | return _class_0.__parent.__init(self, ...) 44 | end, 45 | __base = _base_0, 46 | __name = "PendingPosts", 47 | __parent = _parent_0 48 | }, { 49 | __index = function(cls, name) 50 | local val = rawget(_base_0, name) 51 | if val == nil then 52 | local parent = rawget(cls, "__parent") 53 | if parent then 54 | return parent[name] 55 | end 56 | else 57 | return val 58 | end 59 | end, 60 | __call = function(cls, ...) 61 | local _self_0 = setmetatable({}, _base_0) 62 | cls.__init(_self_0, ...) 63 | return _self_0 64 | end 65 | }) 66 | _base_0.__class = _class_0 67 | if _parent_0.__inherited then 68 | _parent_0.__inherited(_parent_0, _class_0) 69 | end 70 | PendingPosts = _class_0 71 | return _class_0 72 | end 73 | -------------------------------------------------------------------------------- /community/flows/pending_posts.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | 3 | import Flow from require "lapis.flow" 4 | import PendingPosts, ActivityLogs, ModerationLogs from require "community.models" 5 | 6 | class PendingPosts extends Flow 7 | -- this is for when post creator is deleting their own post 8 | delete_pending_post: (pending_post) => 9 | if pending_post\delete! 10 | ActivityLogs\create { 11 | user_id: @current_user.id 12 | object: pending_post 13 | action: "delete" 14 | } 15 | true 16 | 17 | -- this is for when a moderator promotes the post 18 | promote_pending_post: (pending_post) => 19 | post, err = pending_post\promote! 20 | unless post 21 | return nil, err 22 | 23 | ActivityLogs\create { 24 | user_id: @current_user.id 25 | object: pending_post 26 | action: "promote" 27 | data: { 28 | post_id: post.id 29 | } 30 | } 31 | 32 | post 33 | 34 | -------------------------------------------------------------------------------- /community/flows/subscriptions.lua: -------------------------------------------------------------------------------- 1 | local Flow 2 | Flow = require("lapis.flow").Flow 3 | local db = require("lapis.db") 4 | local assert_page 5 | assert_page = require("community.helpers.app").assert_page 6 | local assert_valid 7 | assert_valid = require("lapis.validate").assert_valid 8 | local assert_error 9 | assert_error = require("lapis.application").assert_error 10 | local types = require("lapis.validate.types") 11 | local Subscriptions 12 | Subscriptions = require("community.models").Subscriptions 13 | local preload 14 | preload = require("lapis.db.model").preload 15 | local SubscriptionsFlow 16 | do 17 | local _class_0 18 | local _parent_0 = Flow 19 | local _base_0 = { 20 | expose_assigns = true, 21 | subscribe_to_topic = function(self, topic) 22 | assert_error(topic:allowed_to_view(self.current_user, self._req), "invalid topic") 23 | return topic:subscribe(self.current_user) 24 | end, 25 | subscribe_to_category = function(self, category) 26 | assert_error(category:allowed_to_view(self.current_user, self._req), "invalid category") 27 | return category:subscribe(self.current_user) 28 | end, 29 | find_subscription = function(self) 30 | if self.subscription then 31 | return self.subscription 32 | end 33 | local params = assert_valid(self.params, types.params_shape({ 34 | { 35 | "object_id", 36 | types.db_id 37 | }, 38 | { 39 | "object_type", 40 | types.db_enum(Subscriptions.object_types) 41 | } 42 | })) 43 | self.subscription = Subscriptions:find({ 44 | object_type = Subscriptions.object_types:for_db(params.object_type), 45 | object_id = params.object_id, 46 | user_id = self.current_user.id 47 | }) 48 | return self.subscription 49 | end, 50 | show_subscriptions = function(self) 51 | assert_page(self) 52 | self.pager = Subscriptions:paginated("where ? order by created_at desc", db.clause({ 53 | user_id = self.current_user.id, 54 | subscribed = true 55 | }), { 56 | per_page = 50, 57 | prepare_results = function(subs) 58 | for _index_0 = 1, #subs do 59 | local sub = subs[_index_0] 60 | sub.user = self.current_user 61 | end 62 | preload(subs, "object") 63 | return subs 64 | end 65 | }) 66 | self.subscriptions = self.pager:get_page(self.page) 67 | end 68 | } 69 | _base_0.__index = _base_0 70 | setmetatable(_base_0, _parent_0.__base) 71 | _class_0 = setmetatable({ 72 | __init = function(self, req) 73 | _class_0.__parent.__init(self, req) 74 | return assert(self.current_user, "missing current user for subscription flow") 75 | end, 76 | __base = _base_0, 77 | __name = "SubscriptionsFlow", 78 | __parent = _parent_0 79 | }, { 80 | __index = function(cls, name) 81 | local val = rawget(_base_0, name) 82 | if val == nil then 83 | local parent = rawget(cls, "__parent") 84 | if parent then 85 | return parent[name] 86 | end 87 | else 88 | return val 89 | end 90 | end, 91 | __call = function(cls, ...) 92 | local _self_0 = setmetatable({}, _base_0) 93 | cls.__init(_self_0, ...) 94 | return _self_0 95 | end 96 | }) 97 | _base_0.__class = _class_0 98 | if _parent_0.__inherited then 99 | _parent_0.__inherited(_parent_0, _class_0) 100 | end 101 | SubscriptionsFlow = _class_0 102 | return _class_0 103 | end 104 | -------------------------------------------------------------------------------- /community/flows/subscriptions.moon: -------------------------------------------------------------------------------- 1 | import Flow from require "lapis.flow" 2 | db = require "lapis.db" 3 | 4 | import assert_page from require "community.helpers.app" 5 | import assert_valid from require "lapis.validate" 6 | import assert_error from require "lapis.application" 7 | types = require "lapis.validate.types" 8 | 9 | import Subscriptions from require "community.models" 10 | 11 | import preload from require "lapis.db.model" 12 | 13 | class SubscriptionsFlow extends Flow 14 | expose_assigns: true 15 | 16 | new: (req) => 17 | super req 18 | assert @current_user, "missing current user for subscription flow" 19 | 20 | subscribe_to_topic: (topic) => 21 | assert_error topic\allowed_to_view(@current_user, @_req), "invalid topic" 22 | topic\subscribe @current_user 23 | 24 | subscribe_to_category: (category) => 25 | assert_error category\allowed_to_view(@current_user, @_req), "invalid category" 26 | category\subscribe @current_user 27 | 28 | find_subscription: => 29 | return @subscription if @subscription 30 | 31 | params = assert_valid @params, types.params_shape { 32 | {"object_id", types.db_id} 33 | {"object_type", types.db_enum Subscriptions.object_types} 34 | } 35 | 36 | @subscription = Subscriptions\find { 37 | object_type: Subscriptions.object_types\for_db params.object_type 38 | object_id: params.object_id 39 | user_id: @current_user.id 40 | } 41 | 42 | @subscription 43 | 44 | show_subscriptions: => 45 | assert_page @ 46 | 47 | -- TODO: there's no index on order 48 | @pager = Subscriptions\paginated "where ? order by created_at desc", db.clause({ 49 | user_id: @current_user.id 50 | subscribed: true 51 | }), { 52 | per_page: 50 53 | prepare_results: (subs) -> 54 | for sub in *subs 55 | sub.user = @current_user 56 | 57 | preload subs, "object" 58 | subs 59 | } 60 | 61 | @subscriptions = @pager\get_page @page 62 | 63 | -------------------------------------------------------------------------------- /community/flows/votes.lua: -------------------------------------------------------------------------------- 1 | local Flow 2 | Flow = require("lapis.flow").Flow 3 | local Votes, CommunityUsers 4 | do 5 | local _obj_0 = require("community.models") 6 | Votes, CommunityUsers = _obj_0.Votes, _obj_0.CommunityUsers 7 | end 8 | local db = require("lapis.db") 9 | local assert_error 10 | assert_error = require("lapis.application").assert_error 11 | local assert_valid 12 | assert_valid = require("lapis.validate").assert_valid 13 | local require_current_user 14 | require_current_user = require("community.helpers.app").require_current_user 15 | local types = require("lapis.validate.types") 16 | local VotesFlow 17 | do 18 | local _class_0 19 | local _parent_0 = Flow 20 | local _base_0 = { 21 | expose_assigns = true, 22 | load_object = function(self) 23 | if self.object then 24 | return 25 | end 26 | local params = assert_valid(self.params, types.params_shape({ 27 | { 28 | "object_id", 29 | types.db_id 30 | }, 31 | { 32 | "object_type", 33 | types.db_enum(Votes.object_types) 34 | } 35 | })) 36 | local model = Votes:model_for_object_type(params.object_type) 37 | self.object = model:find(params.object_id) 38 | return assert_error(self.object, "invalid vote object") 39 | end, 40 | vote = require_current_user(function(self) 41 | self:load_object() 42 | if self.params.action then 43 | local params = assert_valid(self.params, types.params_shape({ 44 | { 45 | "action", 46 | types.one_of({ 47 | "remove" 48 | }) 49 | } 50 | })) 51 | local _exp_0 = params.action 52 | if "remove" == _exp_0 then 53 | assert_error(self.object:allowed_to_vote(self.current_user, "remove"), "not allowed to unvote") 54 | Votes:unvote(self.object, self.current_user) 55 | end 56 | else 57 | local params = assert_valid(self.params, types.params_shape({ 58 | { 59 | "direction", 60 | types.one_of({ 61 | "up", 62 | "down" 63 | }) 64 | } 65 | })) 66 | assert_error(self.object:allowed_to_vote(self.current_user, self.params.direction), "not allowed to vote") 67 | self.vote = Votes:vote(self.object, self.current_user, self.params.direction == "up") 68 | assert_error(self.vote, "vote changed in another request") 69 | end 70 | return true 71 | end) 72 | } 73 | _base_0.__index = _base_0 74 | setmetatable(_base_0, _parent_0.__base) 75 | _class_0 = setmetatable({ 76 | __init = function(self, ...) 77 | return _class_0.__parent.__init(self, ...) 78 | end, 79 | __base = _base_0, 80 | __name = "VotesFlow", 81 | __parent = _parent_0 82 | }, { 83 | __index = function(cls, name) 84 | local val = rawget(_base_0, name) 85 | if val == nil then 86 | local parent = rawget(cls, "__parent") 87 | if parent then 88 | return parent[name] 89 | end 90 | else 91 | return val 92 | end 93 | end, 94 | __call = function(cls, ...) 95 | local _self_0 = setmetatable({}, _base_0) 96 | cls.__init(_self_0, ...) 97 | return _self_0 98 | end 99 | }) 100 | _base_0.__class = _class_0 101 | if _parent_0.__inherited then 102 | _parent_0.__inherited(_parent_0, _class_0) 103 | end 104 | VotesFlow = _class_0 105 | return _class_0 106 | end 107 | -------------------------------------------------------------------------------- /community/flows/votes.moon: -------------------------------------------------------------------------------- 1 | import Flow from require "lapis.flow" 2 | import Votes, CommunityUsers from require "community.models" 3 | 4 | db = require "lapis.db" 5 | import assert_error from require "lapis.application" 6 | import assert_valid from require "lapis.validate" 7 | 8 | import require_current_user from require "community.helpers.app" 9 | 10 | types = require "lapis.validate.types" 11 | 12 | class VotesFlow extends Flow 13 | expose_assigns: true 14 | 15 | load_object: => 16 | return if @object 17 | 18 | params = assert_valid @params, types.params_shape { 19 | {"object_id", types.db_id} 20 | {"object_type", types.db_enum Votes.object_types} 21 | } 22 | 23 | model = Votes\model_for_object_type params.object_type 24 | @object = model\find params.object_id 25 | assert_error @object, "invalid vote object" 26 | 27 | vote: require_current_user => 28 | @load_object! 29 | 30 | if @params.action 31 | params = assert_valid @params, types.params_shape { 32 | {"action", types.one_of {"remove"}} 33 | } 34 | 35 | switch params.action 36 | when "remove" 37 | assert_error @object\allowed_to_vote(@current_user, "remove"), 38 | "not allowed to unvote" 39 | 40 | Votes\unvote @object, @current_user 41 | 42 | else 43 | params = assert_valid @params, types.params_shape { 44 | {"direction", types.one_of {"up", "down"}} 45 | } 46 | 47 | assert_error @object\allowed_to_vote(@current_user, @params.direction), 48 | "not allowed to vote" 49 | 50 | @vote = Votes\vote @object, @current_user, @params.direction == "up" 51 | assert_error @vote, "vote changed in another request" 52 | 53 | true 54 | 55 | -------------------------------------------------------------------------------- /community/helpers/app.lua: -------------------------------------------------------------------------------- 1 | local with_params 2 | with_params = require("lapis.validate").with_params 3 | local shapes = require("community.helpers.shapes") 4 | local assert_page = with_params({ 5 | { 6 | "page", 7 | shapes.page_number 8 | } 9 | }, function(self, params) 10 | self.page = params.page 11 | return self.page 12 | end) 13 | local require_current_user 14 | require_current_user = function(fn) 15 | local assert_error 16 | assert_error = require("lapis.application").assert_error 17 | return function(self, ...) 18 | assert_error(self.current_user, "you must be logged in") 19 | return fn(self, ...) 20 | end 21 | end 22 | return { 23 | assert_page = assert_page, 24 | require_current_user = require_current_user 25 | } 26 | -------------------------------------------------------------------------------- /community/helpers/app.moon: -------------------------------------------------------------------------------- 1 | 2 | import with_params from require "lapis.validate" 3 | shapes = require "community.helpers.shapes" 4 | 5 | assert_page = with_params { 6 | {"page", shapes.page_number} 7 | }, (params) => 8 | @page = params.page 9 | @page 10 | 11 | require_current_user = (fn) -> 12 | import assert_error from require "lapis.application" 13 | (...) => 14 | assert_error @current_user, "you must be logged in" 15 | fn @, ... 16 | 17 | {:assert_page, :require_current_user} 18 | -------------------------------------------------------------------------------- /community/helpers/counters.moon: -------------------------------------------------------------------------------- 1 | 2 | -- bulk_increment Things, "views_count", {{id1, 2}, {id2, 2023}} 3 | bulk_increment = (model, column, tuples)-> 4 | db = require "lapis.db" 5 | 6 | table_escaped = db.escape_identifier model\table_name! 7 | column_escaped = db.escape_identifier column 8 | 9 | buffer = { 10 | "UPDATE #{table_escaped} " 11 | "SET #{column_escaped} = #{column_escaped} + increments.amount " 12 | "FROM (VALUES " 13 | } 14 | 15 | for t in *tuples 16 | table.insert buffer, db.escape_literal db.list t 17 | table.insert buffer, ", " 18 | 19 | buffer[#buffer] = nil 20 | 21 | table.insert buffer, ") AS increments (id, amount) WHERE increments.id = #{table_escaped}.id" 22 | db.query table.concat buffer 23 | 24 | class AsyncCounter 25 | SLEEP: 0.01 26 | MAX_TRIES: 10 -- 0.45 seconds to bust 27 | FLUSH_TIME: 5 -- in seconds 28 | 29 | lock_key: "counter_lock" 30 | flush_key: "counter_flush" 31 | 32 | increment_immediately: false 33 | 34 | sync_types: {} 35 | 36 | new: (@dict_name, @opts={}) => 37 | for k,v in pairs @opts 38 | @[k] = v 39 | 40 | return unless @dict_name 41 | return unless ngx 42 | 43 | @dict = assert ngx.shared[@dict_name], "invalid dict name" 44 | 45 | -- runs function when memory is locked, returns retry count 46 | with_lock: (fn) => 47 | i = 0 48 | while true 49 | i += 1 50 | 51 | if @dict\add(@lock_key, true, 30) or i == @MAX_TRIES 52 | success, err = pcall fn 53 | @dict\delete @lock_key 54 | assert success, err 55 | break 56 | 57 | ngx.sleep @SLEEP * i 58 | 59 | if i == @MAX_TRIES 60 | busted_count_key = "#{@lock_key}_busted" 61 | @dict\add busted_count_key, 0 62 | @dict\incr busted_count_key, 1 63 | 64 | i 65 | 66 | increment: (key, amount=1) => 67 | if @increment_immediately 68 | t, id = key\match "(%w+):(%d+)" 69 | if sync = @sync_types[t] 70 | sync { {tonumber(id), amount} } 71 | 72 | return true 73 | 74 | return unless @dict 75 | 76 | @with_lock -> 77 | @dict\add key, 0 78 | @dict\incr key, amount 79 | 80 | if @dict\add @flush_key, true 81 | ngx.timer.at @FLUSH_TIME, -> 82 | @sync! 83 | import run_after_dispatch from require "lapis.nginx.context" 84 | run_after_dispatch! -- manually release resources since we are in new context 85 | 86 | sync: => 87 | counters_synced = 0 88 | 89 | bulk_updates = {} 90 | @with_lock -> 91 | @dict\delete @flush_key 92 | for key in *@dict\get_keys! 93 | t, id = key\match "(%w+):(%d+)" 94 | if t 95 | counters_synced += 1 96 | bulk_updates[t] or= {} 97 | incr = @dict\get key 98 | table.insert bulk_updates[t], {tonumber(id), incr} 99 | @dict\delete key 100 | 101 | for t, updates in pairs bulk_updates 102 | if sync = @sync_types[t] 103 | sync updates 104 | 105 | counters_synced 106 | 107 | { :AsyncCounter, :bulk_increment } 108 | -------------------------------------------------------------------------------- /community/helpers/html.lua: -------------------------------------------------------------------------------- 1 | local is_empty_html 2 | is_empty_html = function(str) 3 | if str:match("%<[iI][mM][gG]%s") then 4 | return false 5 | end 6 | local out = (str:gsub("%<.-%>", ""):gsub(" ", "")) 7 | return not not out:find("^%s*$") 8 | end 9 | return { 10 | is_empty_html = is_empty_html 11 | } 12 | -------------------------------------------------------------------------------- /community/helpers/html.moon: -------------------------------------------------------------------------------- 1 | is_empty_html = (str) -> 2 | -- has an image, not empty 3 | return false if str\match "%<[iI][mM][gG]%s" 4 | 5 | -- only whitespace after html tags removed 6 | out = (str\gsub("%<.-%>", "")\gsub(" ", "")) 7 | not not out\find "^%s*$" 8 | 9 | { :is_empty_html } 10 | -------------------------------------------------------------------------------- /community/helpers/markdown.lua: -------------------------------------------------------------------------------- 1 | local cmark, discount 2 | if pcall(function() 3 | cmark = require("cmark") 4 | end) then 5 | return { 6 | cmark = cmark, 7 | markdown_to_html = function(markdown) 8 | local opts = cmark.OPT_VALIDATE_UTF8 + cmark.OPT_NORMALIZE + cmark.OPT_SMART + cmark.OPT_UNSAFE 9 | local document = assert(cmark.parse_string(markdown, opts)) 10 | return cmark.render_html(document, opts) 11 | end 12 | } 13 | end 14 | if pcall(function() 15 | discount = require("discount") 16 | end) then 17 | return { 18 | discount = discount, 19 | markdown_to_html = function(markdown) 20 | return discount(markdown) 21 | end 22 | } 23 | end 24 | return error("failed to find a markdown library (tried cmark, discount)") 25 | -------------------------------------------------------------------------------- /community/helpers/markdown.moon: -------------------------------------------------------------------------------- 1 | local cmark, discount 2 | 3 | if pcall -> cmark = require "cmark" 4 | return { 5 | :cmark 6 | markdown_to_html: (markdown) -> 7 | opts = cmark.OPT_VALIDATE_UTF8 + 8 | cmark.OPT_NORMALIZE + 9 | cmark.OPT_SMART + 10 | cmark.OPT_UNSAFE -- sanitization is up to the renderer 11 | 12 | document = assert cmark.parse_string markdown, opts 13 | cmark.render_html document, opts 14 | } 15 | 16 | if pcall -> discount = require "discount" 17 | return { 18 | :discount 19 | markdown_to_html: (markdown) -> 20 | discount markdown 21 | } 22 | 23 | error "failed to find a markdown library (tried cmark, discount)" 24 | -------------------------------------------------------------------------------- /community/helpers/shapes.lua: -------------------------------------------------------------------------------- 1 | local types = require("lapis.validate.types") 2 | local empty_html = (types.empty + types.trimmed_text * types.custom(function(str) 3 | local is_empty_html 4 | is_empty_html = require("community.helpers.html").is_empty_html 5 | return is_empty_html(str) 6 | end) / nil):describe("empty html") 7 | local color = types.one_of({ 8 | types.pattern("^#" .. tostring(("[a-fA-F%d]"):rep("6")) .. "$"), 9 | types.pattern("^#" .. tostring(("[a-fA-F%d]"):rep("3")) .. "$") 10 | }):describe("hex color") 11 | local page_number = (types.empty / 1) + (types.one_of({ 12 | types.number / math.floor, 13 | types.string:length(0, 5) * types.pattern("^%d+$") / tonumber 14 | }) * types.range(1, 1000)):describe("page number") 15 | local db_nullable 16 | db_nullable = function(t) 17 | local db = require("lapis.db") 18 | return t + types.empty / db.NULL 19 | end 20 | local default 21 | default = function(value) 22 | if type(value) == "table" then 23 | error("You used table for default value. In order to prevent you from accidentally sharing the same reference across many requests you must pass a function that returns the table") 24 | end 25 | return types.empty / value + types.any 26 | end 27 | local convert_array = types.table / function(t) 28 | local result = { } 29 | local i = 1 30 | while true do 31 | local str_i = tostring(i) 32 | do 33 | local v = t[str_i] or t[i] 34 | if v then 35 | result[i] = v 36 | else 37 | break 38 | end 39 | end 40 | i = i + 1 41 | end 42 | return result 43 | end 44 | return { 45 | empty_html = empty_html, 46 | color = color, 47 | page_number = page_number, 48 | db_nullable = db_nullable, 49 | default = default, 50 | convert_array = convert_array 51 | } 52 | -------------------------------------------------------------------------------- /community/helpers/shapes.moon: -------------------------------------------------------------------------------- 1 | types = require "lapis.validate.types" 2 | 3 | empty_html = (types.empty + types.trimmed_text * types.custom((str) -> 4 | import is_empty_html from require "community.helpers.html" 5 | is_empty_html str 6 | ) / nil)\describe "empty html" 7 | 8 | color = types.one_of({ 9 | types.pattern "^##{"[a-fA-F%d]"\rep "6"}$" 10 | types.pattern "^##{"[a-fA-F%d]"\rep "3"}$" 11 | })\describe "hex color" 12 | 13 | page_number = (types.empty / 1) + (types.one_of({ 14 | types.number / math.floor 15 | types.string\length(0,5) * types.pattern("^%d+$") / tonumber 16 | }) * types.range(1, 1000))\describe "page number" 17 | 18 | db_nullable = (t) -> 19 | db = require "lapis.db" 20 | t + types.empty / db.NULL 21 | 22 | default = (value) -> 23 | if type(value) == "table" 24 | error "You used table for default value. In order to prevent you from accidentally sharing the same reference across many requests you must pass a function that returns the table" 25 | 26 | types.empty / value + types.any 27 | 28 | -- this will create a copy of the table with all string sequential integer 29 | -- fields converted to numbers, essentially extracting the array from the 30 | -- table. Any other fields will be dropped 31 | convert_array = types.table / (t) -> 32 | result = {} 33 | i = 1 34 | 35 | while true 36 | str_i = "#{i}" 37 | if v = t[str_i] or t[i] 38 | result[i] = v 39 | else 40 | break 41 | 42 | i += 1 43 | 44 | result 45 | 46 | 47 | { 48 | :empty_html 49 | :color 50 | :page_number 51 | :db_nullable 52 | :default 53 | :convert_array 54 | } 55 | -------------------------------------------------------------------------------- /community/helpers/unicode.lua: -------------------------------------------------------------------------------- 1 | local P, Cs, R, S 2 | do 3 | local _obj_0 = require("lpeg") 4 | P, Cs, R, S = _obj_0.P, _obj_0.Cs, _obj_0.R, _obj_0.S 5 | end 6 | local cont = R("\128\191") 7 | local utf8_codepoint = R("\194\223") * cont + R("\224\239") * cont * cont + R("\240\244") * cont * cont * cont 8 | local has_utf8_codepoint 9 | do 10 | local p = (1 - utf8_codepoint) ^ 0 * utf8_codepoint 11 | has_utf8_codepoint = function(str) 12 | return not not p:match(str) 13 | end 14 | end 15 | local acceptable_character = S("\r\n\t") + R("\032\126") + utf8_codepoint 16 | local acceptable_string = acceptable_character ^ 0 * P(-1) 17 | local strip_invalid_utf8 18 | do 19 | local p = Cs((R("\0\127") + utf8_codepoint + P(1) / "") ^ 0) 20 | strip_invalid_utf8 = function(text) 21 | return p:match(text) 22 | end 23 | end 24 | local strip_bad_chars 25 | do 26 | local p = Cs((acceptable_character + P(1) / "") ^ 0 * -1) 27 | strip_bad_chars = function(text) 28 | return p:match(text) 29 | end 30 | end 31 | return { 32 | utf8_codepoint = utf8_codepoint, 33 | has_utf8_codepoint = has_utf8_codepoint, 34 | strip_invalid_utf8 = strip_invalid_utf8, 35 | acceptable_character = acceptable_character, 36 | acceptable_string = acceptable_string, 37 | strip_bad_chars = strip_bad_chars 38 | } 39 | -------------------------------------------------------------------------------- /community/helpers/unicode.moon: -------------------------------------------------------------------------------- 1 | import P, Cs, R, S from require "lpeg" 2 | 3 | cont = R("\128\191") 4 | utf8_codepoint = R("\194\223") * cont + 5 | R("\224\239") * cont * cont + 6 | R("\240\244") * cont * cont * cont 7 | 8 | has_utf8_codepoint = do 9 | p = (1 - utf8_codepoint)^0 * utf8_codepoint 10 | (str) -> not not p\match str 11 | 12 | acceptable_character = S("\r\n\t") + R("\032\126") + utf8_codepoint 13 | acceptable_string = acceptable_character^0 * P -1 14 | 15 | strip_invalid_utf8 = do 16 | p = Cs (R("\0\127") + utf8_codepoint + P(1) / "")^0 17 | (text) -> p\match text 18 | 19 | strip_bad_chars = do 20 | p = Cs (acceptable_character + P(1) / "")^0 * -1 21 | (text) -> p\match text 22 | 23 | 24 | {:utf8_codepoint, :has_utf8_codepoint, :strip_invalid_utf8, :acceptable_character, :acceptable_string, :strip_bad_chars} 25 | 26 | -------------------------------------------------------------------------------- /community/limits.lua: -------------------------------------------------------------------------------- 1 | return { 2 | MAX_BODY_LEN = 1024 * 20, 3 | MAX_TITLE_LEN = 256, 4 | POSTS_PER_PAGE = 20, 5 | TOPICS_PER_PAGE = 20, 6 | MAX_TAG_LEN = 30, 7 | MAX_CATEGORY_DEPTH = 4, 8 | MAX_CATEGORY_CHILDREN = 12 9 | } 10 | -------------------------------------------------------------------------------- /community/limits.moon: -------------------------------------------------------------------------------- 1 | { 2 | MAX_BODY_LEN: 1024 * 20 3 | MAX_TITLE_LEN: 256 4 | POSTS_PER_PAGE: 20 5 | TOPICS_PER_PAGE: 20 6 | MAX_TAG_LEN: 30 7 | MAX_CATEGORY_DEPTH: 4 8 | MAX_CATEGORY_CHILDREN: 12 9 | } 10 | -------------------------------------------------------------------------------- /community/model.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import Model from require "lapis.db.model" 4 | import OrderedPaginator from require "lapis.db.pagination" 5 | 6 | import underscore, singularize from require "lapis.util" 7 | 8 | prefix = "community_" 9 | 10 | external_models = { 11 | Users: true 12 | } 13 | 14 | class CommunityModel extends Model 15 | @get_relation_model: (name) => 16 | if external_models[name] 17 | require("models")[name] 18 | else 19 | require("community.models")[name] 20 | 21 | @table_name: => 22 | name = prefix .. underscore @__name 23 | @table_name = -> name 24 | name 25 | 26 | @singular_name: => 27 | name = singularize underscore @__name 28 | @singular_name = -> name 29 | name 30 | 31 | class VirtualModel extends CommunityModel 32 | @table_name: => error "Attempted to get table name for a VirtualModel: these types of models are not backed by a table and have no table name. Please check your relation definition, and avoid calling methods like find/select/create/update" 33 | 34 | -- this makes the method to load fetch or create the virual model instance 35 | @make_loader: (name, fn) => 36 | (key, ...) => 37 | relations = require "lapis.db.model.relations" 38 | -- TODO: setting a relation's cached value should be an interface in lapis 39 | @[relations.LOADED_KEY] or={} 40 | @[relations.LOADED_KEY][name] or= {} 41 | @[relations.LOADED_KEY][name][key] or= fn @, key, ... 42 | @[relations.LOADED_KEY][name][key] 43 | 44 | -- only clear the relations, don't try to fetch any data 45 | refresh: => 46 | relations = require "lapis.db.model.relations" 47 | 48 | if loaded_relations = @[relations.LOADED_KEY] 49 | for name in pairs loaded_relations 50 | relations.clear_loaded_relation @, name 51 | 52 | 53 | class NestedOrderedPaginator extends OrderedPaginator 54 | prepare_results: (items) => 55 | items = super items 56 | 57 | parent_field = @opts.parent_field 58 | child_field = @opts.child_field or "children" 59 | 60 | by_parent = {} 61 | 62 | -- sort and nest 63 | top_level = for item in *items 64 | if pid = item[parent_field] 65 | by_parent[pid] or= {} 66 | table.insert by_parent[pid], item 67 | 68 | if @opts.is_top_level_item 69 | continue unless @opts.is_top_level_item item 70 | else 71 | continue if item[parent_field] 72 | 73 | item 74 | 75 | for item in *items 76 | item[child_field] = by_parent[item.id] 77 | if children = @opts.sort and item[child_field] 78 | @opts.sort children 79 | 80 | top_level 81 | 82 | select: (q, opts) => 83 | tname = db.escape_identifier @model\table_name! 84 | parent_field = assert @opts.parent_field, "missing parent_field" 85 | child_field = @opts.child_field or "children" 86 | 87 | child_clause = { 88 | [db.raw "pr.#{db.escape_identifier parent_field}"]: db.raw "nested.id" 89 | } 90 | 91 | if clause = @opts.child_clause 92 | for k,v in pairs clause 93 | field_name = if type(k) == "string" 94 | db.raw "pr.#{db.escape_identifier k}" 95 | else 96 | k 97 | 98 | child_clause[field_name] = v 99 | 100 | base_fields = @opts.base_fields or "*" 101 | recursive_fields = @opts.recursive_fields or "pr.*" 102 | 103 | res = db.query " 104 | with recursive nested as ( 105 | (select #{base_fields} from #{tname} #{q}) 106 | union 107 | select #{recursive_fields} from #{tname} pr, nested 108 | where #{db.encode_clause child_clause} 109 | ) 110 | select * from nested 111 | " 112 | 113 | for r in *res 114 | @model\load r 115 | 116 | res 117 | 118 | prefix_table = (table_name) -> 119 | prefix .. table_name 120 | 121 | { Model: CommunityModel, :VirtualModel, :NestedOrderedPaginator, :prefix_table } 122 | -------------------------------------------------------------------------------- /community/models.lua: -------------------------------------------------------------------------------- 1 | local autoload, underscore 2 | do 3 | local _obj_0 = require("lapis.util") 4 | autoload, underscore = _obj_0.autoload, _obj_0.underscore 5 | end 6 | local community_models = autoload("community.models") 7 | local loadkit = require("loadkit") 8 | return setmetatable({ }, { 9 | __index = function(self, model_name) 10 | local base_model = community_models[model_name] 11 | if not (base_model) then 12 | error("Failed to find community model: " .. tostring(model_name)) 13 | end 14 | local override_module = "models.community." .. tostring(underscore(model_name)) 15 | local fname = loadkit.make_loader("lua")(override_module) 16 | local custom_model 17 | if fname then 18 | custom_model = assert(loadfile(fname))(base_model) 19 | end 20 | self[model_name] = custom_model or base_model 21 | return self[model_name] 22 | end 23 | }) 24 | -------------------------------------------------------------------------------- /community/models.moon: -------------------------------------------------------------------------------- 1 | import autoload, underscore from require "lapis.util" 2 | 3 | community_models = autoload "community.models" 4 | 5 | loadkit = require "loadkit" 6 | 7 | -- this will first load a model from community/models/X.lua 8 | -- it will then check to see if there's a overriden version in the models/community/X.lua 9 | -- if there is an overriden file, it's pass the reference to the origial model 10 | 11 | setmetatable {}, __index: (model_name) => 12 | base_model = community_models[model_name] 13 | 14 | unless base_model 15 | error "Failed to find community model: #{model_name}" 16 | 17 | override_module = "models.community.#{underscore model_name}" 18 | 19 | fname = loadkit.make_loader("lua") override_module 20 | custom_model = if fname 21 | assert(loadfile(fname)) base_model 22 | 23 | @[model_name] = custom_model or base_model 24 | @[model_name] 25 | 26 | -------------------------------------------------------------------------------- /community/models/activity_logs.lua: -------------------------------------------------------------------------------- 1 | local enum 2 | enum = require("lapis.db.model").enum 3 | local Model 4 | Model = require("community.model").Model 5 | local to_json 6 | to_json = require("lapis.util").to_json 7 | local ActivityLogs 8 | do 9 | local _class_0 10 | local _parent_0 = Model 11 | local _base_0 = { 12 | action_name = function(self) 13 | return self.__class.actions[self.__class.object_types:to_name(self.object_type)][self.action] 14 | end 15 | } 16 | _base_0.__index = _base_0 17 | setmetatable(_base_0, _parent_0.__base) 18 | _class_0 = setmetatable({ 19 | __init = function(self, ...) 20 | return _class_0.__parent.__init(self, ...) 21 | end, 22 | __base = _base_0, 23 | __name = "ActivityLogs", 24 | __parent = _parent_0 25 | }, { 26 | __index = function(cls, name) 27 | local val = rawget(_base_0, name) 28 | if val == nil then 29 | local parent = rawget(cls, "__parent") 30 | if parent then 31 | return parent[name] 32 | end 33 | else 34 | return val 35 | end 36 | end, 37 | __call = function(cls, ...) 38 | local _self_0 = setmetatable({}, _base_0) 39 | cls.__init(_self_0, ...) 40 | return _self_0 41 | end 42 | }) 43 | _base_0.__class = _class_0 44 | local self = _class_0 45 | self.timestamp = true 46 | self.actions = { 47 | topic = enum({ 48 | create = 1, 49 | delete = 2 50 | }), 51 | post = enum({ 52 | create = 1, 53 | delete = 2, 54 | edit = 3, 55 | vote = 4 56 | }), 57 | category = enum({ 58 | create = 1, 59 | edit = 2 60 | }), 61 | pending_post = enum({ 62 | create_post = 1, 63 | create_topic = 2, 64 | delete = 3, 65 | promote = 4 66 | }) 67 | } 68 | self.relations = { 69 | { 70 | "user", 71 | belongs_to = "Users" 72 | }, 73 | { 74 | "object", 75 | polymorphic_belongs_to = { 76 | [1] = { 77 | "topic", 78 | "Topics" 79 | }, 80 | [2] = { 81 | "post", 82 | "Posts" 83 | }, 84 | [3] = { 85 | "category", 86 | "Categories" 87 | }, 88 | [4] = { 89 | "pending_post", 90 | "PendingPosts" 91 | } 92 | } 93 | } 94 | } 95 | self.create = function(self, opts) 96 | if opts == nil then 97 | opts = { } 98 | end 99 | assert(opts.user_id, "missing user_id") 100 | assert(opts.action, "missing action") 101 | if opts.object then 102 | local object = assert(opts.object, "missing object") 103 | opts.object = nil 104 | opts.object_id = assert(object.id, "object does not have id") 105 | opts.object_type = self:object_type_for_object(object) 106 | end 107 | opts.object_type = self.object_types:for_db(opts.object_type) 108 | local type_name = self.object_types:to_name(opts.object_type) 109 | local actions = self.actions[type_name] 110 | if not (actions) then 111 | error("missing action for type: " .. tostring(type_name)) 112 | end 113 | opts.action = actions:for_db(opts.action) 114 | if opts.data then 115 | local db_json 116 | db_json = require("community.helpers.models").db_json 117 | opts.data = db_json(opts.data) 118 | end 119 | if not (opts.ip) then 120 | local CommunityUsers 121 | CommunityUsers = require("community.models").CommunityUsers 122 | opts.ip = CommunityUsers:current_ip_address() 123 | end 124 | return _class_0.__parent.create(self, opts) 125 | end 126 | if _parent_0.__inherited then 127 | _parent_0.__inherited(_parent_0, _class_0) 128 | end 129 | ActivityLogs = _class_0 130 | return _class_0 131 | end 132 | -------------------------------------------------------------------------------- /community/models/activity_logs.moon: -------------------------------------------------------------------------------- 1 | import enum from require "lapis.db.model" 2 | import Model from require "community.model" 3 | import to_json from require "lapis.util" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_activity_logs ( 8 | -- id integer NOT NULL, 9 | -- user_id integer NOT NULL, 10 | -- object_type integer DEFAULT 0 NOT NULL, 11 | -- object_id integer NOT NULL, 12 | -- action integer DEFAULT 0 NOT NULL, 13 | -- data jsonb, 14 | -- created_at timestamp without time zone NOT NULL, 15 | -- updated_at timestamp without time zone NOT NULL, 16 | -- ip inet 17 | -- ); 18 | -- ALTER TABLE ONLY community_activity_logs 19 | -- ADD CONSTRAINT community_activity_logs_pkey PRIMARY KEY (id); 20 | -- CREATE INDEX community_activity_logs_object_type_object_id_idx ON community_activity_logs USING btree (object_type, object_id); 21 | -- CREATE INDEX community_activity_logs_user_id_id_idx ON community_activity_logs USING btree (user_id, id); 22 | -- 23 | class ActivityLogs extends Model 24 | @timestamp: true 25 | 26 | @actions: { 27 | topic: enum { 28 | create: 1 29 | delete: 2 30 | } 31 | 32 | post: enum { 33 | create: 1 34 | delete: 2 35 | edit: 3 36 | vote: 4 37 | } 38 | 39 | category: enum { 40 | create: 1 41 | edit: 2 42 | } 43 | 44 | pending_post: enum { 45 | create_post: 1 46 | create_topic: 2 47 | delete: 3 48 | promote: 4 49 | } 50 | } 51 | 52 | @relations: { 53 | {"user", belongs_to: "Users"} 54 | 55 | {"object", polymorphic_belongs_to: { 56 | [1]: {"topic", "Topics"} 57 | [2]: {"post", "Posts"} 58 | [3]: {"category", "Categories"} 59 | [4]: {"pending_post", "PendingPosts"} 60 | }} 61 | } 62 | 63 | @create: (opts={}) => 64 | assert opts.user_id, "missing user_id" 65 | assert opts.action, "missing action" 66 | 67 | if opts.object 68 | object = assert opts.object, "missing object" 69 | opts.object = nil 70 | opts.object_id = assert object.id, "object does not have id" 71 | opts.object_type = @object_type_for_object object 72 | 73 | opts.object_type = @object_types\for_db opts.object_type 74 | 75 | type_name = @object_types\to_name opts.object_type 76 | actions = @actions[type_name] 77 | unless actions 78 | error "missing action for type: #{type_name}" 79 | opts.action = actions\for_db opts.action 80 | 81 | if opts.data 82 | import db_json from require "community.helpers.models" 83 | opts.data = db_json opts.data 84 | 85 | unless opts.ip 86 | import CommunityUsers from require "community.models" 87 | opts.ip = CommunityUsers\current_ip_address! 88 | 89 | super opts 90 | 91 | action_name: => 92 | @@actions[@@object_types\to_name @object_type][@action] 93 | 94 | -------------------------------------------------------------------------------- /community/models/bans.lua: -------------------------------------------------------------------------------- 1 | local enum 2 | enum = require("lapis.db.model").enum 3 | local Model 4 | Model = require("community.model").Model 5 | local insert_on_conflict_ignore 6 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 7 | local Bans 8 | do 9 | local _class_0 10 | local _parent_0 = Model 11 | local _base_0 = { } 12 | _base_0.__index = _base_0 13 | setmetatable(_base_0, _parent_0.__base) 14 | _class_0 = setmetatable({ 15 | __init = function(self, ...) 16 | return _class_0.__parent.__init(self, ...) 17 | end, 18 | __base = _base_0, 19 | __name = "Bans", 20 | __parent = _parent_0 21 | }, { 22 | __index = function(cls, name) 23 | local val = rawget(_base_0, name) 24 | if val == nil then 25 | local parent = rawget(cls, "__parent") 26 | if parent then 27 | return parent[name] 28 | end 29 | else 30 | return val 31 | end 32 | end, 33 | __call = function(cls, ...) 34 | local _self_0 = setmetatable({}, _base_0) 35 | cls.__init(_self_0, ...) 36 | return _self_0 37 | end 38 | }) 39 | _base_0.__class = _class_0 40 | local self = _class_0 41 | self.timestamp = true 42 | self.primary_key = { 43 | "object_type", 44 | "object_id", 45 | "banned_user_id" 46 | } 47 | self.relations = { 48 | { 49 | "banned_user", 50 | belongs_to = "Users" 51 | }, 52 | { 53 | "banning_user", 54 | belongs_to = "Users" 55 | }, 56 | { 57 | "object", 58 | polymorphic_belongs_to = { 59 | [1] = { 60 | "category", 61 | "Categories" 62 | }, 63 | [2] = { 64 | "topic", 65 | "Topics" 66 | }, 67 | [3] = { 68 | "category_group", 69 | "CategoryGroups" 70 | } 71 | } 72 | } 73 | } 74 | self.find_for_object = function(self, object, user) 75 | if not (user) then 76 | return nil 77 | end 78 | return Bans:find({ 79 | object_type = self:object_type_for_object(object), 80 | object_id = object.id, 81 | banned_user_id = user.id 82 | }) 83 | end 84 | self.create = function(self, opts) 85 | assert(opts.object, "missing object") 86 | opts.object_id = opts.object.id 87 | opts.object_type = self:object_type_for_object(opts.object) 88 | opts.object = nil 89 | return insert_on_conflict_ignore(self, opts) 90 | end 91 | if _parent_0.__inherited then 92 | _parent_0.__inherited(_parent_0, _class_0) 93 | end 94 | Bans = _class_0 95 | return _class_0 96 | end 97 | -------------------------------------------------------------------------------- /community/models/bans.moon: -------------------------------------------------------------------------------- 1 | 2 | import enum from require "lapis.db.model" 3 | import Model from require "community.model" 4 | 5 | import insert_on_conflict_ignore from require "community.helpers.models" 6 | 7 | -- Generated schema dump: (do not edit) 8 | -- 9 | -- CREATE TABLE community_bans ( 10 | -- object_type integer DEFAULT 0 NOT NULL, 11 | -- object_id integer NOT NULL, 12 | -- banned_user_id integer NOT NULL, 13 | -- reason text, 14 | -- banning_user_id integer, 15 | -- created_at timestamp without time zone NOT NULL, 16 | -- updated_at timestamp without time zone NOT NULL 17 | -- ); 18 | -- ALTER TABLE ONLY community_bans 19 | -- ADD CONSTRAINT community_bans_pkey PRIMARY KEY (object_type, object_id, banned_user_id); 20 | -- CREATE INDEX community_bans_banned_user_id_idx ON community_bans USING btree (banned_user_id); 21 | -- CREATE INDEX community_bans_banning_user_id_idx ON community_bans USING btree (banning_user_id); 22 | -- CREATE INDEX community_bans_object_type_object_id_created_at_idx ON community_bans USING btree (object_type, object_id, created_at); 23 | -- 24 | class Bans extends Model 25 | @timestamp: true 26 | @primary_key: {"object_type", "object_id", "banned_user_id"} 27 | 28 | @relations: { 29 | {"banned_user", belongs_to: "Users"} 30 | {"banning_user", belongs_to: "Users"} 31 | 32 | {"object", polymorphic_belongs_to: { 33 | [1]: {"category", "Categories"} 34 | [2]: {"topic", "Topics"} 35 | [3]: {"category_group", "CategoryGroups"} 36 | }} 37 | } 38 | 39 | @find_for_object: (object, user) => 40 | return nil unless user 41 | Bans\find { 42 | object_type: @object_type_for_object object 43 | object_id: object.id 44 | banned_user_id: user.id 45 | } 46 | 47 | @create: (opts) => 48 | assert opts.object, "missing object" 49 | 50 | opts.object_id = opts.object.id 51 | opts.object_type = @object_type_for_object opts.object 52 | opts.object = nil 53 | 54 | insert_on_conflict_ignore @, opts 55 | 56 | -------------------------------------------------------------------------------- /community/models/blocks.lua: -------------------------------------------------------------------------------- 1 | local Model 2 | Model = require("community.model").Model 3 | local insert_on_conflict_ignore 4 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 5 | local Blocks 6 | do 7 | local _class_0 8 | local _parent_0 = Model 9 | local _base_0 = { } 10 | _base_0.__index = _base_0 11 | setmetatable(_base_0, _parent_0.__base) 12 | _class_0 = setmetatable({ 13 | __init = function(self, ...) 14 | return _class_0.__parent.__init(self, ...) 15 | end, 16 | __base = _base_0, 17 | __name = "Blocks", 18 | __parent = _parent_0 19 | }, { 20 | __index = function(cls, name) 21 | local val = rawget(_base_0, name) 22 | if val == nil then 23 | local parent = rawget(cls, "__parent") 24 | if parent then 25 | return parent[name] 26 | end 27 | else 28 | return val 29 | end 30 | end, 31 | __call = function(cls, ...) 32 | local _self_0 = setmetatable({}, _base_0) 33 | cls.__init(_self_0, ...) 34 | return _self_0 35 | end 36 | }) 37 | _base_0.__class = _class_0 38 | local self = _class_0 39 | self.primary_key = { 40 | "blocking_user_id", 41 | "blocked_user_id" 42 | } 43 | self.timestamp = true 44 | self.relations = { 45 | { 46 | "blocking_user", 47 | belongs_to = "Users" 48 | }, 49 | { 50 | "blocked_user", 51 | belongs_to = "Users" 52 | } 53 | } 54 | self.create = insert_on_conflict_ignore 55 | if _parent_0.__inherited then 56 | _parent_0.__inherited(_parent_0, _class_0) 57 | end 58 | Blocks = _class_0 59 | return _class_0 60 | end 61 | -------------------------------------------------------------------------------- /community/models/blocks.moon: -------------------------------------------------------------------------------- 1 | 2 | import Model from require "community.model" 3 | import insert_on_conflict_ignore from require "community.helpers.models" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_blocks ( 8 | -- blocking_user_id integer NOT NULL, 9 | -- blocked_user_id integer NOT NULL, 10 | -- created_at timestamp without time zone NOT NULL, 11 | -- updated_at timestamp without time zone NOT NULL 12 | -- ); 13 | -- ALTER TABLE ONLY community_blocks 14 | -- ADD CONSTRAINT community_blocks_pkey PRIMARY KEY (blocking_user_id, blocked_user_id); 15 | -- 16 | class Blocks extends Model 17 | @primary_key: {"blocking_user_id", "blocked_user_id"} 18 | @timestamp: true 19 | 20 | @relations: { 21 | {"blocking_user", belongs_to: "Users"} 22 | {"blocked_user", belongs_to: "Users"} 23 | } 24 | 25 | @create: insert_on_conflict_ignore 26 | -------------------------------------------------------------------------------- /community/models/bookmarks.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local insert_on_conflict_ignore 5 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 6 | local Bookmarks 7 | do 8 | local _class_0 9 | local _parent_0 = Model 10 | local _base_0 = { } 11 | _base_0.__index = _base_0 12 | setmetatable(_base_0, _parent_0.__base) 13 | _class_0 = setmetatable({ 14 | __init = function(self, ...) 15 | return _class_0.__parent.__init(self, ...) 16 | end, 17 | __base = _base_0, 18 | __name = "Bookmarks", 19 | __parent = _parent_0 20 | }, { 21 | __index = function(cls, name) 22 | local val = rawget(_base_0, name) 23 | if val == nil then 24 | local parent = rawget(cls, "__parent") 25 | if parent then 26 | return parent[name] 27 | end 28 | else 29 | return val 30 | end 31 | end, 32 | __call = function(cls, ...) 33 | local _self_0 = setmetatable({}, _base_0) 34 | cls.__init(_self_0, ...) 35 | return _self_0 36 | end 37 | }) 38 | _base_0.__class = _class_0 39 | local self = _class_0 40 | self.primary_key = { 41 | "user_id", 42 | "object_type", 43 | "object_id" 44 | } 45 | self.timestamp = true 46 | self.relations = { 47 | { 48 | "user", 49 | belongs_to = "Users" 50 | }, 51 | { 52 | "object", 53 | polymorphic_belongs_to = { 54 | [1] = { 55 | "user", 56 | "Users" 57 | }, 58 | [2] = { 59 | "topic", 60 | "Topics" 61 | }, 62 | [3] = { 63 | "post", 64 | "Posts" 65 | } 66 | } 67 | } 68 | } 69 | self.create = function(self, opts) 70 | if opts == nil then 71 | opts = { } 72 | end 73 | opts.object_type = self.object_types:for_db(opts.object_type) 74 | return insert_on_conflict_ignore(self, opts) 75 | end 76 | self.get = function(self, object, user) 77 | if not (user) then 78 | return nil 79 | end 80 | return self:find({ 81 | user_id = user.id, 82 | object_id = object.id, 83 | object_type = self:object_type_for_model(object.__class) 84 | }) 85 | end 86 | self.save = function(self, object, user) 87 | if not (user) then 88 | return 89 | end 90 | return self:create({ 91 | user_id = user.id, 92 | object_id = object.id, 93 | object_type = self:object_type_for_model(object.__class) 94 | }) 95 | end 96 | self.remove = function(self, object, user) 97 | do 98 | local bookmark = self:get(object, user) 99 | if bookmark then 100 | return bookmark:delete() 101 | end 102 | end 103 | end 104 | if _parent_0.__inherited then 105 | _parent_0.__inherited(_parent_0, _class_0) 106 | end 107 | Bookmarks = _class_0 108 | return _class_0 109 | end 110 | -------------------------------------------------------------------------------- /community/models/bookmarks.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import Model from require "community.model" 3 | 4 | import insert_on_conflict_ignore from require "community.helpers.models" 5 | 6 | -- Generated schema dump: (do not edit) 7 | -- 8 | -- CREATE TABLE community_bookmarks ( 9 | -- user_id integer NOT NULL, 10 | -- object_type integer DEFAULT 0 NOT NULL, 11 | -- object_id integer NOT NULL, 12 | -- created_at timestamp without time zone NOT NULL, 13 | -- updated_at timestamp without time zone NOT NULL 14 | -- ); 15 | -- ALTER TABLE ONLY community_bookmarks 16 | -- ADD CONSTRAINT community_bookmarks_pkey PRIMARY KEY (user_id, object_type, object_id); 17 | -- CREATE INDEX community_bookmarks_user_id_created_at_idx ON community_bookmarks USING btree (user_id, created_at); 18 | -- 19 | class Bookmarks extends Model 20 | @primary_key: { "user_id", "object_type", "object_id" } 21 | @timestamp: true 22 | 23 | @relations: { 24 | {"user", belongs_to: "Users"} 25 | {"object", polymorphic_belongs_to: { 26 | [1]: {"user", "Users"} 27 | [2]: {"topic", "Topics"} 28 | [3]: {"post", "Posts"} 29 | }} 30 | } 31 | 32 | @create: (opts={}) => 33 | opts.object_type = @object_types\for_db opts.object_type 34 | insert_on_conflict_ignore @, opts 35 | 36 | @get: (object, user) => 37 | return nil unless user 38 | @find { 39 | user_id: user.id 40 | object_id: object.id 41 | object_type: @object_type_for_model object.__class 42 | } 43 | 44 | @save: (object, user) => 45 | return unless user 46 | 47 | @create { 48 | user_id: user.id 49 | object_id: object.id 50 | object_type: @object_type_for_model object.__class 51 | } 52 | 53 | @remove: (object, user) => 54 | if bookmark = @get object, user 55 | bookmark\delete! 56 | 57 | -------------------------------------------------------------------------------- /community/models/category_group_categories.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local enum 3 | enum = require("lapis.db.model").enum 4 | local Model 5 | Model = require("community.model").Model 6 | local insert_on_conflict_ignore 7 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 8 | local CategoryGroupCategories 9 | do 10 | local _class_0 11 | local _parent_0 = Model 12 | local _base_0 = { } 13 | _base_0.__index = _base_0 14 | setmetatable(_base_0, _parent_0.__base) 15 | _class_0 = setmetatable({ 16 | __init = function(self, ...) 17 | return _class_0.__parent.__init(self, ...) 18 | end, 19 | __base = _base_0, 20 | __name = "CategoryGroupCategories", 21 | __parent = _parent_0 22 | }, { 23 | __index = function(cls, name) 24 | local val = rawget(_base_0, name) 25 | if val == nil then 26 | local parent = rawget(cls, "__parent") 27 | if parent then 28 | return parent[name] 29 | end 30 | else 31 | return val 32 | end 33 | end, 34 | __call = function(cls, ...) 35 | local _self_0 = setmetatable({}, _base_0) 36 | cls.__init(_self_0, ...) 37 | return _self_0 38 | end 39 | }) 40 | _base_0.__class = _class_0 41 | local self = _class_0 42 | self.timestamp = true 43 | self.primary_key = { 44 | "category_group_id", 45 | "category_id" 46 | } 47 | self.relations = { 48 | { 49 | "category_group", 50 | belongs_to = "CategoryGroups" 51 | }, 52 | { 53 | "category", 54 | belongs_to = "Categories" 55 | } 56 | } 57 | self.create = insert_on_conflict_ignore 58 | if _parent_0.__inherited then 59 | _parent_0.__inherited(_parent_0, _class_0) 60 | end 61 | CategoryGroupCategories = _class_0 62 | return _class_0 63 | end 64 | -------------------------------------------------------------------------------- /community/models/category_group_categories.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import enum from require "lapis.db.model" 4 | import Model from require "community.model" 5 | 6 | import insert_on_conflict_ignore from require "community.helpers.models" 7 | 8 | -- Generated schema dump: (do not edit) 9 | -- 10 | -- CREATE TABLE community_category_group_categories ( 11 | -- category_group_id integer NOT NULL, 12 | -- category_id integer NOT NULL, 13 | -- created_at timestamp without time zone NOT NULL, 14 | -- updated_at timestamp without time zone NOT NULL 15 | -- ); 16 | -- ALTER TABLE ONLY community_category_group_categories 17 | -- ADD CONSTRAINT community_category_group_categories_pkey PRIMARY KEY (category_group_id, category_id); 18 | -- CREATE UNIQUE INDEX community_category_group_categories_category_id_idx ON community_category_group_categories USING btree (category_id); 19 | -- 20 | class CategoryGroupCategories extends Model 21 | @timestamp: true 22 | @primary_key: {"category_group_id", "category_id"} 23 | 24 | @relations: { 25 | {"category_group", belongs_to: "CategoryGroups"} 26 | {"category", belongs_to: "Categories"} 27 | } 28 | 29 | @create: insert_on_conflict_ignore 30 | 31 | 32 | -------------------------------------------------------------------------------- /community/models/category_members.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local CategoryMembers 5 | do 6 | local _class_0 7 | local _parent_0 = Model 8 | local _base_0 = { } 9 | _base_0.__index = _base_0 10 | setmetatable(_base_0, _parent_0.__base) 11 | _class_0 = setmetatable({ 12 | __init = function(self, ...) 13 | return _class_0.__parent.__init(self, ...) 14 | end, 15 | __base = _base_0, 16 | __name = "CategoryMembers", 17 | __parent = _parent_0 18 | }, { 19 | __index = function(cls, name) 20 | local val = rawget(_base_0, name) 21 | if val == nil then 22 | local parent = rawget(cls, "__parent") 23 | if parent then 24 | return parent[name] 25 | end 26 | else 27 | return val 28 | end 29 | end, 30 | __call = function(cls, ...) 31 | local _self_0 = setmetatable({}, _base_0) 32 | cls.__init(_self_0, ...) 33 | return _self_0 34 | end 35 | }) 36 | _base_0.__class = _class_0 37 | local self = _class_0 38 | self.timestamp = true 39 | self.primary_key = { 40 | "user_id", 41 | "category_id" 42 | } 43 | self.relations = { 44 | { 45 | "user", 46 | belongs_to = "Users" 47 | }, 48 | { 49 | "category", 50 | belongs_to = "Categories" 51 | } 52 | } 53 | self.create = function(self, opts) 54 | if opts == nil then 55 | opts = { } 56 | end 57 | assert(opts.user_id, "missing user id") 58 | assert(opts.category_id, "missing category id") 59 | local insert_on_conflict_ignore 60 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 61 | return insert_on_conflict_ignore(self, opts) 62 | end 63 | if _parent_0.__inherited then 64 | _parent_0.__inherited(_parent_0, _class_0) 65 | end 66 | CategoryMembers = _class_0 67 | return _class_0 68 | end 69 | -------------------------------------------------------------------------------- /community/models/category_members.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import Model from require "community.model" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_category_members ( 8 | -- user_id integer NOT NULL, 9 | -- category_id integer NOT NULL, 10 | -- accepted boolean DEFAULT false NOT NULL, 11 | -- created_at timestamp without time zone NOT NULL, 12 | -- updated_at timestamp without time zone NOT NULL 13 | -- ); 14 | -- ALTER TABLE ONLY community_category_members 15 | -- ADD CONSTRAINT community_category_members_pkey PRIMARY KEY (user_id, category_id); 16 | -- CREATE INDEX community_category_members_category_id_user_id_idx ON community_category_members USING btree (category_id, user_id) WHERE accepted; 17 | -- 18 | class CategoryMembers extends Model 19 | @timestamp: true 20 | @primary_key: {"user_id", "category_id"} 21 | 22 | @relations: { 23 | {"user", belongs_to: "Users"} 24 | {"category", belongs_to: "Categories"} 25 | } 26 | 27 | @create: (opts={}) => 28 | assert opts.user_id, "missing user id" 29 | assert opts.category_id, "missing category id" 30 | 31 | import insert_on_conflict_ignore from require "community.helpers.models" 32 | insert_on_conflict_ignore @, opts 33 | 34 | -------------------------------------------------------------------------------- /community/models/category_post_logs.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import Model from require "community.model" 4 | 5 | import insert_on_conflict_ignore from require "community.helpers.models" 6 | 7 | -- Generated schema dump: (do not edit) 8 | -- 9 | -- CREATE TABLE community_category_post_logs ( 10 | -- category_id integer NOT NULL, 11 | -- post_id integer NOT NULL 12 | -- ); 13 | -- ALTER TABLE ONLY community_category_post_logs 14 | -- ADD CONSTRAINT community_category_post_logs_pkey PRIMARY KEY (category_id, post_id); 15 | -- CREATE INDEX community_category_post_logs_post_id_idx ON community_category_post_logs USING btree (post_id); 16 | -- 17 | class CategoryPostLogs extends Model 18 | @primary_key: {"category_id", "post_id"} 19 | 20 | @relations: { 21 | {"post", belongs_to: "Posts"} 22 | {"category", belongs_to: "Categories"} 23 | } 24 | 25 | @categories_to_log: (category) => 26 | category_ids = [c.id for c in *category\get_ancestors! when c\should_log_posts!] 27 | if category\should_log_posts! 28 | table.insert category_ids, category.id 29 | 30 | category_ids 31 | 32 | @log_post: (post) => 33 | topic = post\get_topic! 34 | return unless topic 35 | category = topic\get_category! 36 | return unless category 37 | 38 | category_ids = @categories_to_log category 39 | return unless next category_ids 40 | 41 | tuples = for id in *category_ids 42 | db.interpolate_query "?", db.list {post.id, id} 43 | 44 | tbl = db.escape_identifier @table_name! 45 | db.query " 46 | insert into #{tbl} (post_id, category_id) 47 | values #{table.concat tuples, ", "} 48 | on conflict do nothing 49 | ", post.id 50 | 51 | @log_topic_posts: (topic) => 52 | category = topic\get_category! 53 | return unless category 54 | 55 | category_ids = @categories_to_log category 56 | return unless next category_ids 57 | 58 | tuples = for id in *category_ids 59 | db.interpolate_query "?", db.list {id} 60 | 61 | import Posts from require "community.models" 62 | 63 | tbl = db.escape_identifier @table_name! 64 | db.query " 65 | insert into #{tbl} (post_id, category_id) 66 | select topic_post_ids.post_id, category_ids.category_id from 67 | (select id as post_id from #{db.escape_identifier Posts\table_name!} 68 | where topic_id = ? and status = 1 and not deleted) as topic_post_ids(post_id), 69 | (values #{table.concat tuples, ", "}) as category_ids(category_id) 70 | on conflict do nothing 71 | ", topic.id 72 | 73 | @clear_post: (post) => 74 | db.delete @table_name!, { 75 | post_id: post.id 76 | } 77 | 78 | @clear_posts_for_topic: (topic) => 79 | import Posts from require "community.models" 80 | 81 | db.delete @table_name!, { 82 | post_id: db.list { 83 | db.raw db.interpolate_query " 84 | select id from #{db.escape_identifier Posts\table_name!} where topic_id = ? 85 | ", topic.id 86 | } 87 | } 88 | 89 | @create: insert_on_conflict_ignore 90 | -------------------------------------------------------------------------------- /community/models/category_tags.lua: -------------------------------------------------------------------------------- 1 | local Model 2 | Model = require("community.model").Model 3 | local CategoryTags 4 | do 5 | local _class_0 6 | local _parent_0 = Model 7 | local _base_0 = { 8 | name_for_display = function(self) 9 | return self.label or self.slug 10 | end 11 | } 12 | _base_0.__index = _base_0 13 | setmetatable(_base_0, _parent_0.__base) 14 | _class_0 = setmetatable({ 15 | __init = function(self, ...) 16 | return _class_0.__parent.__init(self, ...) 17 | end, 18 | __base = _base_0, 19 | __name = "CategoryTags", 20 | __parent = _parent_0 21 | }, { 22 | __index = function(cls, name) 23 | local val = rawget(_base_0, name) 24 | if val == nil then 25 | local parent = rawget(cls, "__parent") 26 | if parent then 27 | return parent[name] 28 | end 29 | else 30 | return val 31 | end 32 | end, 33 | __call = function(cls, ...) 34 | local _self_0 = setmetatable({}, _base_0) 35 | cls.__init(_self_0, ...) 36 | return _self_0 37 | end 38 | }) 39 | _base_0.__class = _class_0 40 | local self = _class_0 41 | self.timestamp = true 42 | self.relations = { 43 | { 44 | "category", 45 | belongs_to = "Categories" 46 | } 47 | } 48 | self.slugify = function(self, str) 49 | str = str:gsub("%s+", "-") 50 | str = str:gsub("[^%w%-_%.]+", "") 51 | str = str:gsub("^[%-%._]+", "") 52 | str = str:gsub("[%-%._]+$", "") 53 | if str == "" then 54 | return nil 55 | end 56 | str = str:lower() 57 | return str 58 | end 59 | self.create = function(self, opts) 60 | if opts == nil then 61 | opts = { } 62 | end 63 | if opts.label and not opts.slug then 64 | opts.slug = self:slugify(opts.label) 65 | if not (opts.slug) then 66 | return nil, "invalid label" 67 | end 68 | end 69 | if opts.slug == opts.label then 70 | opts.label = nil 71 | end 72 | return _class_0.__parent.create(self, opts) 73 | end 74 | if _parent_0.__inherited then 75 | _parent_0.__inherited(_parent_0, _class_0) 76 | end 77 | CategoryTags = _class_0 78 | return _class_0 79 | end 80 | -------------------------------------------------------------------------------- /community/models/category_tags.moon: -------------------------------------------------------------------------------- 1 | import Model from require "community.model" 2 | 3 | -- Generated schema dump: (do not edit) 4 | -- 5 | -- CREATE TABLE community_category_tags ( 6 | -- id integer NOT NULL, 7 | -- category_id integer NOT NULL, 8 | -- slug character varying(255) NOT NULL, 9 | -- label text, 10 | -- color character varying(255), 11 | -- image_url character varying(255), 12 | -- tag_order integer DEFAULT 0 NOT NULL, 13 | -- created_at timestamp without time zone NOT NULL, 14 | -- updated_at timestamp without time zone NOT NULL, 15 | -- description text 16 | -- ); 17 | -- ALTER TABLE ONLY community_category_tags 18 | -- ADD CONSTRAINT community_category_tags_pkey PRIMARY KEY (id); 19 | -- CREATE UNIQUE INDEX community_category_tags_category_id_slug_idx ON community_category_tags USING btree (category_id, slug); 20 | -- 21 | class CategoryTags extends Model 22 | @timestamp: true 23 | 24 | @relations: { 25 | {"category", belongs_to: "Categories"} 26 | } 27 | 28 | @slugify: (str) => 29 | str = str\gsub "%s+", "-" 30 | str = str\gsub "[^%w%-_%.]+", "" 31 | str = str\gsub "^[%-%._]+", "" 32 | str = str\gsub "[%-%._]+$", "" 33 | return nil if str == "" 34 | 35 | str = str\lower! 36 | str 37 | 38 | @create: (opts={}) => 39 | if opts.label and not opts.slug 40 | opts.slug = @slugify opts.label 41 | return nil, "invalid label" unless opts.slug 42 | 43 | if opts.slug == opts.label 44 | opts.label = nil 45 | 46 | super opts 47 | 48 | name_for_display: => 49 | @label or @slug 50 | -------------------------------------------------------------------------------- /community/models/moderation_log_objects.lua: -------------------------------------------------------------------------------- 1 | local enum 2 | enum = require("lapis.db.model").enum 3 | local Model 4 | Model = require("community.model").Model 5 | local insert_on_conflict_ignore 6 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 7 | local ModerationLogObjects 8 | do 9 | local _class_0 10 | local _parent_0 = Model 11 | local _base_0 = { } 12 | _base_0.__index = _base_0 13 | setmetatable(_base_0, _parent_0.__base) 14 | _class_0 = setmetatable({ 15 | __init = function(self, ...) 16 | return _class_0.__parent.__init(self, ...) 17 | end, 18 | __base = _base_0, 19 | __name = "ModerationLogObjects", 20 | __parent = _parent_0 21 | }, { 22 | __index = function(cls, name) 23 | local val = rawget(_base_0, name) 24 | if val == nil then 25 | local parent = rawget(cls, "__parent") 26 | if parent then 27 | return parent[name] 28 | end 29 | else 30 | return val 31 | end 32 | end, 33 | __call = function(cls, ...) 34 | local _self_0 = setmetatable({}, _base_0) 35 | cls.__init(_self_0, ...) 36 | return _self_0 37 | end 38 | }) 39 | _base_0.__class = _class_0 40 | local self = _class_0 41 | self.timestamp = true 42 | self.relations = { 43 | { 44 | "moderation_log", 45 | belongs_to = "ModerationLogs" 46 | }, 47 | { 48 | "object", 49 | polymorphic_belongs_to = { 50 | [1] = { 51 | "user", 52 | "Users" 53 | }, 54 | [2] = { 55 | "topic", 56 | "Topics" 57 | } 58 | } 59 | } 60 | } 61 | self.create = insert_on_conflict_ignore 62 | if _parent_0.__inherited then 63 | _parent_0.__inherited(_parent_0, _class_0) 64 | end 65 | ModerationLogObjects = _class_0 66 | return _class_0 67 | end 68 | -------------------------------------------------------------------------------- /community/models/moderation_log_objects.moon: -------------------------------------------------------------------------------- 1 | import enum from require "lapis.db.model" 2 | import Model from require "community.model" 3 | 4 | import insert_on_conflict_ignore from require "community.helpers.models" 5 | 6 | -- Generated schema dump: (do not edit) 7 | -- 8 | -- CREATE TABLE community_moderation_log_objects ( 9 | -- moderation_log_id integer NOT NULL, 10 | -- object_type integer DEFAULT 0 NOT NULL, 11 | -- object_id integer NOT NULL, 12 | -- created_at timestamp without time zone NOT NULL, 13 | -- updated_at timestamp without time zone NOT NULL 14 | -- ); 15 | -- ALTER TABLE ONLY community_moderation_log_objects 16 | -- ADD CONSTRAINT community_moderation_log_objects_pkey PRIMARY KEY (moderation_log_id, object_type, object_id); 17 | -- 18 | class ModerationLogObjects extends Model 19 | @timestamp: true 20 | 21 | @relations: { 22 | {"moderation_log", belongs_to: "ModerationLogs"} 23 | 24 | {"object", polymorphic_belongs_to: { 25 | [1]: {"user", "Users"} 26 | [2]: {"topic", "Topics"} 27 | }} 28 | } 29 | 30 | @create: insert_on_conflict_ignore 31 | -------------------------------------------------------------------------------- /community/models/moderators.lua: -------------------------------------------------------------------------------- 1 | local Model 2 | Model = require("community.model").Model 3 | local insert_on_conflict_ignore 4 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 5 | local Moderators 6 | do 7 | local _class_0 8 | local _parent_0 = Model 9 | local _base_0 = { 10 | can_moderate = function(self) 11 | return self.accepted 12 | end 13 | } 14 | _base_0.__index = _base_0 15 | setmetatable(_base_0, _parent_0.__base) 16 | _class_0 = setmetatable({ 17 | __init = function(self, ...) 18 | return _class_0.__parent.__init(self, ...) 19 | end, 20 | __base = _base_0, 21 | __name = "Moderators", 22 | __parent = _parent_0 23 | }, { 24 | __index = function(cls, name) 25 | local val = rawget(_base_0, name) 26 | if val == nil then 27 | local parent = rawget(cls, "__parent") 28 | if parent then 29 | return parent[name] 30 | end 31 | else 32 | return val 33 | end 34 | end, 35 | __call = function(cls, ...) 36 | local _self_0 = setmetatable({}, _base_0) 37 | cls.__init(_self_0, ...) 38 | return _self_0 39 | end 40 | }) 41 | _base_0.__class = _class_0 42 | local self = _class_0 43 | self.timestamp = true 44 | self.primary_key = { 45 | "user_id", 46 | "object_type", 47 | "object_id" 48 | } 49 | self.relations = { 50 | { 51 | "object", 52 | polymorphic_belongs_to = { 53 | [1] = { 54 | "category", 55 | "Categories" 56 | }, 57 | [2] = { 58 | "category_group", 59 | "CategoryGroups" 60 | } 61 | } 62 | }, 63 | { 64 | "user", 65 | belongs_to = "Users" 66 | } 67 | } 68 | self.create = function(self, opts) 69 | if opts == nil then 70 | opts = { } 71 | end 72 | assert(opts.user_id, "missing user_id") 73 | if opts.object then 74 | opts.object_id = opts.object.id 75 | opts.object_type = self:object_type_for_object(opts.object) 76 | opts.object = nil 77 | else 78 | assert(opts.object_id, "missing object_id") 79 | opts.object_type = self.object_types:for_db(opts.object_type) 80 | end 81 | return insert_on_conflict_ignore(self, opts) 82 | end 83 | self.find_for_object_user = function(self, object, user) 84 | if not (object) then 85 | return nil, "invalid object" 86 | end 87 | if not (user) then 88 | return nil, "invalid user" 89 | end 90 | return self:find({ 91 | object_type = self:object_type_for_object(object), 92 | object_id = object.id, 93 | user_id = user.id 94 | }) 95 | end 96 | if _parent_0.__inherited then 97 | _parent_0.__inherited(_parent_0, _class_0) 98 | end 99 | Moderators = _class_0 100 | return _class_0 101 | end 102 | -------------------------------------------------------------------------------- /community/models/moderators.moon: -------------------------------------------------------------------------------- 1 | import Model from require "community.model" 2 | 3 | import insert_on_conflict_ignore from require "community.helpers.models" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_moderators ( 8 | -- user_id integer NOT NULL, 9 | -- object_type integer NOT NULL, 10 | -- object_id integer NOT NULL, 11 | -- admin boolean DEFAULT false NOT NULL, 12 | -- accepted boolean DEFAULT false NOT NULL, 13 | -- created_at timestamp without time zone NOT NULL, 14 | -- updated_at timestamp without time zone NOT NULL 15 | -- ); 16 | -- ALTER TABLE ONLY community_moderators 17 | -- ADD CONSTRAINT community_moderators_pkey PRIMARY KEY (user_id, object_type, object_id); 18 | -- CREATE INDEX community_moderators_object_type_object_id_created_at_idx ON community_moderators USING btree (object_type, object_id, created_at); 19 | -- 20 | class Moderators extends Model 21 | @timestamp: true 22 | @primary_key: {"user_id", "object_type", "object_id"} 23 | 24 | -- all moderatable objects must implement the following methods: 25 | -- \allowed_to_edit_moderators(user) 26 | -- \allowed_to_moderate(user, ignore_admin) 27 | 28 | @relations: { 29 | {"object", polymorphic_belongs_to: { 30 | [1]: {"category", "Categories"} 31 | [2]: {"category_group", "CategoryGroups"} 32 | }} 33 | 34 | {"user", belongs_to: "Users"} 35 | } 36 | 37 | @create: (opts={}) => 38 | assert opts.user_id, "missing user_id" 39 | 40 | if opts.object 41 | opts.object_id = opts.object.id 42 | opts.object_type = @object_type_for_object opts.object 43 | opts.object = nil 44 | else 45 | assert opts.object_id, "missing object_id" 46 | opts.object_type = @object_types\for_db opts.object_type 47 | 48 | insert_on_conflict_ignore @, opts 49 | 50 | @find_for_object_user: (object, user) => 51 | return nil, "invalid object" unless object 52 | return nil, "invalid user" unless user 53 | 54 | @find { 55 | object_type: @object_type_for_object object 56 | object_id: object.id 57 | user_id: user.id 58 | } 59 | 60 | can_moderate: => 61 | @accepted 62 | 63 | 64 | -------------------------------------------------------------------------------- /community/models/poll_choices.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | 3 | import Model, VirtualModel from require "community.model" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_poll_choices ( 8 | -- id integer NOT NULL, 9 | -- poll_id integer NOT NULL, 10 | -- choice_text text NOT NULL, 11 | -- description text, 12 | -- vote_count integer DEFAULT 0 NOT NULL, 13 | -- created_at timestamp without time zone NOT NULL, 14 | -- updated_at timestamp without time zone NOT NULL, 15 | -- "position" integer DEFAULT 0 NOT NULL 16 | -- ); 17 | -- ALTER TABLE ONLY community_poll_choices 18 | -- ADD CONSTRAINT community_poll_choices_pkey PRIMARY KEY (id); 19 | -- CREATE INDEX community_poll_choices_poll_id_idx ON community_poll_choices USING btree (poll_id); 20 | -- 21 | class PollChoices extends Model 22 | @timestamp: true 23 | 24 | class PollChoiceVoters extends VirtualModel 25 | @primary_key: {"poll_choice_id", "user_id"} 26 | 27 | @relations: { 28 | {"poll_choice", belongs_to: "PollChoices"} 29 | {"user", belongs_to: "Users"} 30 | {"vote", has_one: "PollVotes", key: {"poll_choice_id", "user_id"}} 31 | } 32 | 33 | @relations: { 34 | {"poll", belongs_to: "TopicPolls"} 35 | {"poll_votes", has_many: "PollVotes", key: "poll_choice_id"} 36 | } 37 | 38 | with_user: VirtualModel\make_loader "voters", (user_id) => 39 | assert user_id, "expecting user id" 40 | PollChoiceVoters\load { 41 | user_id: user_id 42 | poll_choice_id: @id 43 | } 44 | 45 | name_for_display: => 46 | @choice_text 47 | 48 | recount: => 49 | import PollVotes from require "community.models" 50 | @update { 51 | vote_count: db.raw "(select count(*) 52 | from #{db.escape_identifier PollVotes\table_name!} 53 | where poll_choice_id = #{db.escape_identifier @@table_name!}.id and counted = true)" 54 | } 55 | 56 | delete: => 57 | if super! 58 | -- delete all votes for this choice 59 | import PollVotes from require "community.models" 60 | db.delete PollVotes\table_name!, db.clause { 61 | {"poll_choice_id = ?", @id} 62 | } 63 | true 64 | 65 | --- Set the vote for the user, aware of vote_type for the poll 66 | --- @param user User The user who is voting 67 | --- @return PollVotes The vote if it was created 68 | vote: (user, counted=true) => 69 | assert user, "missing user" 70 | import TopicPolls, PollVotes from require "community.models" 71 | 72 | poll = @get_poll! 73 | return nil, "poll is closed" unless poll\is_open! 74 | 75 | -- Create the vote 76 | vote = PollVotes\create { 77 | poll_choice_id: @id 78 | user_id: user.id 79 | :counted 80 | } 81 | 82 | unless vote 83 | return nil, "could not create vote" 84 | 85 | -- if vote_type is single, clear out other votes 86 | if poll.vote_type == TopicPolls.vote_types.single 87 | other_votes = PollVotes\select db.clause { 88 | {"user_id = ?", user.id} 89 | {"poll_choice_id in (select id from #{db.escape_identifier PollChoices\table_name!} where poll_id = ?)", poll.id} 90 | {"id != ?", vote.id} 91 | } 92 | for other_vote in *other_votes 93 | other_vote\delete! 94 | 95 | vote 96 | 97 | -------------------------------------------------------------------------------- /community/models/poll_votes.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local PollVotes 5 | do 6 | local _class_0 7 | local _parent_0 = Model 8 | local _base_0 = { 9 | create = function(self, opts) 10 | if opts == nil then 11 | opts = { } 12 | end 13 | opts.created_at = opts.created_at or db.format_date() 14 | opts.updated_at = opts.updated_at or db.format_date() 15 | local res = unpack(db.insert(self.__class:table_name(), opts, { 16 | on_conflict = "do_nothing", 17 | returning = "*" 18 | })) 19 | if not (res) then 20 | return nil, "vote already exists" 21 | end 22 | if res.counted then 23 | local PollChoices 24 | PollChoices = require("community.models").PollChoices 25 | db.update(PollChoices:table_name(), { 26 | vote_count = db.raw("vote_count + 1") 27 | }, db.clause({ 28 | { 29 | "id = ?", 30 | res.poll_choice_id 31 | } 32 | })) 33 | end 34 | return self:load(res) 35 | end, 36 | delete = function(self) 37 | local deleted, res = _class_0.__parent.__base.delete(self, db.raw("*")) 38 | if deleted then 39 | local removed_row = unpack(res) 40 | if removed_row.counted then 41 | local PollChoices 42 | PollChoices = require("community.models").PollChoices 43 | db.update(PollChoices:table_name(), { 44 | vote_count = db.raw("vote_count - 1") 45 | }, db.clause({ 46 | { 47 | "id = ?", 48 | removed_row.poll_choice_id 49 | } 50 | })) 51 | end 52 | return true 53 | end 54 | end, 55 | set_counted = function(self, counted) 56 | local updated = self:update({ 57 | counted = counted 58 | }, { 59 | where = db.clause({ 60 | { 61 | "counted = ?", 62 | not counted 63 | } 64 | }) 65 | }) 66 | if updated then 67 | local delta 68 | if counted then 69 | delta = 1 70 | else 71 | delta = -1 72 | end 73 | local PollChoices 74 | PollChoices = require("community.models").PollChoices 75 | db.update(PollChoices:table_name(), { 76 | vote_count = db.raw(db.interpolate_query("vote_count + ?", delta)) 77 | }, db.clause({ 78 | { 79 | "id = ?", 80 | self.poll_choice_id 81 | } 82 | })) 83 | return true 84 | end 85 | end 86 | } 87 | _base_0.__index = _base_0 88 | setmetatable(_base_0, _parent_0.__base) 89 | _class_0 = setmetatable({ 90 | __init = function(self, ...) 91 | return _class_0.__parent.__init(self, ...) 92 | end, 93 | __base = _base_0, 94 | __name = "PollVotes", 95 | __parent = _parent_0 96 | }, { 97 | __index = function(cls, name) 98 | local val = rawget(_base_0, name) 99 | if val == nil then 100 | local parent = rawget(cls, "__parent") 101 | if parent then 102 | return parent[name] 103 | end 104 | else 105 | return val 106 | end 107 | end, 108 | __call = function(cls, ...) 109 | local _self_0 = setmetatable({}, _base_0) 110 | cls.__init(_self_0, ...) 111 | return _self_0 112 | end 113 | }) 114 | _base_0.__class = _class_0 115 | local self = _class_0 116 | self.timestamp = true 117 | self.relations = { 118 | { 119 | "poll_choice", 120 | belongs_to = "PollChoices" 121 | }, 122 | { 123 | "user", 124 | belongs_to = "Users" 125 | } 126 | } 127 | if _parent_0.__inherited then 128 | _parent_0.__inherited(_parent_0, _class_0) 129 | end 130 | PollVotes = _class_0 131 | return _class_0 132 | end 133 | -------------------------------------------------------------------------------- /community/models/poll_votes.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | 3 | import Model from require "community.model" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_poll_votes ( 8 | -- id integer NOT NULL, 9 | -- poll_choice_id integer NOT NULL, 10 | -- user_id integer NOT NULL, 11 | -- counted boolean DEFAULT true NOT NULL, 12 | -- created_at timestamp without time zone NOT NULL, 13 | -- updated_at timestamp without time zone NOT NULL 14 | -- ); 15 | -- ALTER TABLE ONLY community_poll_votes 16 | -- ADD CONSTRAINT community_poll_votes_pkey PRIMARY KEY (id); 17 | -- CREATE UNIQUE INDEX community_poll_votes_poll_choice_id_user_id_idx ON community_poll_votes USING btree (poll_choice_id, user_id); 18 | -- 19 | class PollVotes extends Model 20 | @timestamp: true 21 | 22 | @relations: { 23 | {"poll_choice", belongs_to: "PollChoices"} 24 | {"user", belongs_to: "Users"} 25 | } 26 | 27 | create: (opts={}) => 28 | opts.created_at or= db.format_date! 29 | opts.updated_at or= db.format_date! 30 | 31 | res = unpack db.insert @@table_name!, opts, { 32 | on_conflict: "do_nothing" 33 | returning: "*" 34 | } 35 | 36 | unless res 37 | return nil, "vote already exists" 38 | 39 | if res.counted 40 | -- increment the vote count on the poll choice 41 | import PollChoices from require "community.models" 42 | db.update PollChoices\table_name!, { 43 | vote_count: db.raw "vote_count + 1" 44 | }, db.clause { 45 | {"id = ?", res.poll_choice_id} 46 | } 47 | 48 | @load res 49 | 50 | delete: => 51 | deleted, res = super db.raw "*" 52 | 53 | if deleted 54 | removed_row = unpack res 55 | if removed_row.counted 56 | -- decrement the vote count on the poll choice 57 | import PollChoices from require "community.models" 58 | db.update PollChoices\table_name!, { 59 | vote_count: db.raw "vote_count - 1" 60 | }, db.clause { 61 | {"id = ?", removed_row.poll_choice_id} 62 | } 63 | 64 | true 65 | 66 | -- update the counted field and correctly increment the vote count 67 | set_counted: (counted) => 68 | updated = @update { 69 | counted: counted 70 | }, where: db.clause { 71 | {"counted = ?", not counted} 72 | } 73 | 74 | -- update the counter on the pool choice 75 | if updated 76 | delta = if counted then 1 else -1 77 | 78 | import PollChoices from require "community.models" 79 | db.update PollChoices\table_name!, { 80 | vote_count: db.raw db.interpolate_query "vote_count + ?", delta 81 | }, db.clause { 82 | {"id = ?", @poll_choice_id} 83 | } 84 | true 85 | 86 | -------------------------------------------------------------------------------- /community/models/post_edits.lua: -------------------------------------------------------------------------------- 1 | local Model 2 | Model = require("community.model").Model 3 | local PostEdits 4 | do 5 | local _class_0 6 | local _parent_0 = Model 7 | local _base_0 = { } 8 | _base_0.__index = _base_0 9 | setmetatable(_base_0, _parent_0.__base) 10 | _class_0 = setmetatable({ 11 | __init = function(self, ...) 12 | return _class_0.__parent.__init(self, ...) 13 | end, 14 | __base = _base_0, 15 | __name = "PostEdits", 16 | __parent = _parent_0 17 | }, { 18 | __index = function(cls, name) 19 | local val = rawget(_base_0, name) 20 | if val == nil then 21 | local parent = rawget(cls, "__parent") 22 | if parent then 23 | return parent[name] 24 | end 25 | else 26 | return val 27 | end 28 | end, 29 | __call = function(cls, ...) 30 | local _self_0 = setmetatable({}, _base_0) 31 | cls.__init(_self_0, ...) 32 | return _self_0 33 | end 34 | }) 35 | _base_0.__class = _class_0 36 | local self = _class_0 37 | self.timestamp = true 38 | self.relations = { 39 | { 40 | "post", 41 | belongs_to = "Posts" 42 | }, 43 | { 44 | "user", 45 | belongs_to = "Users" 46 | } 47 | } 48 | self.create = function(self, opts) 49 | if opts == nil then 50 | opts = { } 51 | end 52 | assert(opts.post_id, "missing post_id") 53 | assert(opts.user_id, "missing user_id") 54 | assert(opts.body_before, "missing body_before") 55 | local Posts 56 | Posts = require("community.models").Posts 57 | opts.body_format = opts.body_format or Posts.body_formats.html 58 | return _class_0.__parent.create(self, opts) 59 | end 60 | if _parent_0.__inherited then 61 | _parent_0.__inherited(_parent_0, _class_0) 62 | end 63 | PostEdits = _class_0 64 | return _class_0 65 | end 66 | -------------------------------------------------------------------------------- /community/models/post_edits.moon: -------------------------------------------------------------------------------- 1 | 2 | import Model from require "community.model" 3 | 4 | -- Generated schema dump: (do not edit) 5 | -- 6 | -- CREATE TABLE community_post_edits ( 7 | -- id integer NOT NULL, 8 | -- post_id integer NOT NULL, 9 | -- user_id integer NOT NULL, 10 | -- body_before text NOT NULL, 11 | -- reason text, 12 | -- created_at timestamp without time zone NOT NULL, 13 | -- updated_at timestamp without time zone NOT NULL, 14 | -- body_format smallint DEFAULT 1 NOT NULL 15 | -- ); 16 | -- ALTER TABLE ONLY community_post_edits 17 | -- ADD CONSTRAINT community_post_edits_pkey PRIMARY KEY (id); 18 | -- CREATE UNIQUE INDEX community_post_edits_post_id_id_idx ON community_post_edits USING btree (post_id, id); 19 | -- 20 | class PostEdits extends Model 21 | @timestamp: true 22 | 23 | @relations: { 24 | {"post", belongs_to: "Posts"} 25 | {"user", belongs_to: "Users"} 26 | } 27 | 28 | @create: (opts={}) => 29 | assert opts.post_id, "missing post_id" 30 | assert opts.user_id, "missing user_id" 31 | assert opts.body_before, "missing body_before" 32 | import Posts from require "community.models" 33 | opts.body_format or= Posts.body_formats.html 34 | super opts 35 | 36 | -------------------------------------------------------------------------------- /community/models/post_reports.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local enum 3 | enum = require("lapis.db.model").enum 4 | local Model 5 | Model = require("community.model").Model 6 | local PostReports 7 | do 8 | local _class_0 9 | local _parent_0 = Model 10 | local _base_0 = { 11 | is_resolved = function(self) 12 | return self.status == self.__class.statuses.resolved 13 | end, 14 | is_pending = function(self) 15 | return self.status == self.__class.statuses.pending 16 | end, 17 | is_ignored = function(self) 18 | return self.status == self.__class.statuses.ignored 19 | end, 20 | delete = function(self, ...) 21 | local ModerationLogs 22 | ModerationLogs = require("community.models").ModerationLogs 23 | db.delete(ModerationLogs:table_name(), { 24 | object_type = assert(ModerationLogs.object_types.post_report), 25 | object_id = assert(self.id) 26 | }) 27 | return _class_0.__parent.__base.delete(self, ...) 28 | end 29 | } 30 | _base_0.__index = _base_0 31 | setmetatable(_base_0, _parent_0.__base) 32 | _class_0 = setmetatable({ 33 | __init = function(self, ...) 34 | return _class_0.__parent.__init(self, ...) 35 | end, 36 | __base = _base_0, 37 | __name = "PostReports", 38 | __parent = _parent_0 39 | }, { 40 | __index = function(cls, name) 41 | local val = rawget(_base_0, name) 42 | if val == nil then 43 | local parent = rawget(cls, "__parent") 44 | if parent then 45 | return parent[name] 46 | end 47 | else 48 | return val 49 | end 50 | end, 51 | __call = function(cls, ...) 52 | local _self_0 = setmetatable({}, _base_0) 53 | cls.__init(_self_0, ...) 54 | return _self_0 55 | end 56 | }) 57 | _base_0.__class = _class_0 58 | local self = _class_0 59 | self.timestamp = true 60 | self.statuses = enum({ 61 | pending = 1, 62 | resolved = 2, 63 | ignored = 3 64 | }) 65 | self.reasons = enum({ 66 | other = 1, 67 | off_topic = 2, 68 | spam = 3, 69 | offensive = 4 70 | }) 71 | self.relations = { 72 | { 73 | "category", 74 | belongs_to = "Categories" 75 | }, 76 | { 77 | "post", 78 | belongs_to = "Posts" 79 | }, 80 | { 81 | "user", 82 | belongs_to = "Users" 83 | }, 84 | { 85 | "moderating_user", 86 | belongs_to = "Users" 87 | }, 88 | { 89 | "post_user", 90 | belongs_to = "Users" 91 | }, 92 | { 93 | "post_topic", 94 | belongs_to = "Topics" 95 | } 96 | } 97 | self.create = function(self, opts) 98 | if opts == nil then 99 | opts = { } 100 | end 101 | opts.status = opts.status or "pending" 102 | opts.status = self.statuses:for_db(opts.status) 103 | opts.reason = self.reasons:for_db(opts.reason) 104 | local tname = self:table_name() 105 | if opts.category_id then 106 | opts.category_report_number = db.raw(db.interpolate_query("\n coalesce(\n (select category_report_number\n from " .. tostring(db.escape_identifier(tname)) .. " where category_id = ? order by id desc limit 1\n ), 0) + 1\n ", opts.category_id)) 107 | end 108 | assert(opts.post_id, "missing post_id") 109 | assert(opts.user_id, "missing user_id") 110 | return _class_0.__parent.create(self, opts) 111 | end 112 | if _parent_0.__inherited then 113 | _parent_0.__inherited(_parent_0, _class_0) 114 | end 115 | PostReports = _class_0 116 | return _class_0 117 | end 118 | -------------------------------------------------------------------------------- /community/models/post_reports.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import enum from require "lapis.db.model" 4 | import Model from require "community.model" 5 | 6 | -- Generated schema dump: (do not edit) 7 | -- 8 | -- CREATE TABLE community_post_reports ( 9 | -- id integer NOT NULL, 10 | -- category_id integer, 11 | -- post_id integer NOT NULL, 12 | -- user_id integer NOT NULL, 13 | -- category_report_number integer DEFAULT 0 NOT NULL, 14 | -- moderating_user_id integer, 15 | -- status integer DEFAULT 0 NOT NULL, 16 | -- reason integer DEFAULT 0 NOT NULL, 17 | -- body text, 18 | -- created_at timestamp without time zone NOT NULL, 19 | -- updated_at timestamp without time zone NOT NULL, 20 | -- moderated_at timestamp without time zone, 21 | -- post_user_id integer, 22 | -- post_parent_post_id integer, 23 | -- post_body text, 24 | -- post_body_format smallint, 25 | -- post_topic_id integer 26 | -- ); 27 | -- ALTER TABLE ONLY community_post_reports 28 | -- ADD CONSTRAINT community_post_reports_pkey PRIMARY KEY (id); 29 | -- CREATE INDEX community_post_reports_category_id_id_idx ON community_post_reports USING btree (category_id, id) WHERE (category_id IS NOT NULL); 30 | -- CREATE INDEX community_post_reports_post_id_id_status_idx ON community_post_reports USING btree (post_id, id, status); 31 | -- CREATE INDEX community_post_reports_post_user_id_idx ON community_post_reports USING btree (post_user_id) WHERE (post_user_id IS NOT NULL); 32 | -- 33 | class PostReports extends Model 34 | @timestamp: true 35 | 36 | @statuses: enum { 37 | pending: 1 38 | resolved: 2 39 | ignored: 3 40 | } 41 | 42 | @reasons: enum { 43 | other: 1 44 | off_topic: 2 45 | spam: 3 46 | offensive: 4 47 | } 48 | 49 | @relations: { 50 | {"category", belongs_to: "Categories"} 51 | {"post", belongs_to: "Posts"} 52 | {"user", belongs_to: "Users"} 53 | {"moderating_user", belongs_to: "Users"} 54 | {"post_user", belongs_to: "Users"} 55 | {"post_topic", belongs_to: "Topics"} 56 | } 57 | 58 | @create: (opts={}) => 59 | opts.status or= "pending" 60 | opts.status = @statuses\for_db opts.status 61 | 62 | opts.reason = @reasons\for_db opts.reason 63 | 64 | tname = @table_name! 65 | 66 | if opts.category_id 67 | opts.category_report_number = db.raw db.interpolate_query " 68 | coalesce( 69 | (select category_report_number 70 | from #{db.escape_identifier tname} where category_id = ? order by id desc limit 1 71 | ), 0) + 1 72 | ", opts.category_id 73 | 74 | assert opts.post_id, "missing post_id" 75 | assert opts.user_id, "missing user_id" 76 | 77 | super opts 78 | 79 | is_resolved: => @status == @@statuses.resolved 80 | is_pending: => @status == @@statuses.pending 81 | is_ignored: => @status == @@statuses.ignored 82 | 83 | delete: (...) => 84 | import ModerationLogs from require "community.models" 85 | 86 | db.delete ModerationLogs\table_name!, { 87 | object_type: assert ModerationLogs.object_types.post_report 88 | object_id: assert @id 89 | } 90 | 91 | super ... 92 | 93 | -------------------------------------------------------------------------------- /community/models/posts_search.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local insert_on_conflict_update 5 | insert_on_conflict_update = require("community.helpers.models").insert_on_conflict_update 6 | local PostsSearch 7 | do 8 | local _class_0 9 | local _parent_0 = Model 10 | local _base_0 = { } 11 | _base_0.__index = _base_0 12 | setmetatable(_base_0, _parent_0.__base) 13 | _class_0 = setmetatable({ 14 | __init = function(self, ...) 15 | return _class_0.__parent.__init(self, ...) 16 | end, 17 | __base = _base_0, 18 | __name = "PostsSearch", 19 | __parent = _parent_0 20 | }, { 21 | __index = function(cls, name) 22 | local val = rawget(_base_0, name) 23 | if val == nil then 24 | local parent = rawget(cls, "__parent") 25 | if parent then 26 | return parent[name] 27 | end 28 | else 29 | return val 30 | end 31 | end, 32 | __call = function(cls, ...) 33 | local _self_0 = setmetatable({}, _base_0) 34 | cls.__init(_self_0, ...) 35 | return _self_0 36 | end 37 | }) 38 | _base_0.__class = _class_0 39 | local self = _class_0 40 | self.primary_key = "post_id" 41 | self.index_lang = "english" 42 | self.relations = { 43 | { 44 | "post", 45 | belongs_to = "Posts" 46 | } 47 | } 48 | self.index_post = function(self, post) 49 | local Extractor 50 | Extractor = require("web_sanitize.html").Extractor 51 | local extract_text = Extractor({ 52 | printable = true 53 | }) 54 | local topic = post:get_topic() 55 | local body = extract_text(post.body) 56 | local title 57 | if post:is_topic_post() then 58 | title = topic.title 59 | end 60 | local words 61 | if title then 62 | words = db.interpolate_query("setweight(to_tsvector(?, ?), 'A') || setweight(to_tsvector(?, ?), 'B')", self.index_lang, title, self.index_lang, body) 63 | else 64 | words = db.interpolate_query("to_tsvector(?, ?)", self.index_lang, body) 65 | end 66 | return insert_on_conflict_update(self, { 67 | post_id = post.id 68 | }, { 69 | topic_id = topic.id, 70 | category_id = topic.category_id, 71 | words = db.raw(words), 72 | posted_at = post.created_at 73 | }) 74 | end 75 | if _parent_0.__inherited then 76 | _parent_0.__inherited(_parent_0, _class_0) 77 | end 78 | PostsSearch = _class_0 79 | return _class_0 80 | end 81 | -------------------------------------------------------------------------------- /community/models/posts_search.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import Model from require "community.model" 3 | import insert_on_conflict_update from require "community.helpers.models" 4 | 5 | 6 | -- Generated schema dump: (do not edit) 7 | -- 8 | -- CREATE TABLE community_posts_search ( 9 | -- post_id integer NOT NULL, 10 | -- topic_id integer NOT NULL, 11 | -- category_id integer, 12 | -- posted_at timestamp without time zone NOT NULL, 13 | -- words tsvector 14 | -- ); 15 | -- ALTER TABLE ONLY community_posts_search 16 | -- ADD CONSTRAINT community_posts_search_pkey PRIMARY KEY (post_id); 17 | -- CREATE INDEX community_posts_search_post_id_idx ON community_posts_search USING btree (post_id); 18 | -- CREATE INDEX community_posts_search_words_idx ON community_posts_search USING gin (words); 19 | -- 20 | class PostsSearch extends Model 21 | @primary_key: "post_id" 22 | @index_lang: "english" 23 | 24 | @relations: { 25 | {"post", belongs_to: "Posts"} 26 | } 27 | 28 | @index_post: (post) => 29 | import Extractor from require "web_sanitize.html" 30 | extract_text = Extractor printable: true 31 | topic = post\get_topic! 32 | 33 | body = extract_text post.body 34 | 35 | title = if post\is_topic_post! 36 | topic.title 37 | 38 | words = if title 39 | db.interpolate_query "setweight(to_tsvector(?, ?), 'A') || setweight(to_tsvector(?, ?), 'B')", 40 | @index_lang, 41 | title, 42 | @index_lang, 43 | body 44 | else 45 | db.interpolate_query "to_tsvector(?, ?)", @index_lang, body 46 | 47 | insert_on_conflict_update @, { 48 | post_id: post.id 49 | }, { 50 | topic_id: topic.id 51 | category_id: topic.category_id 52 | words: db.raw words 53 | posted_at: post.created_at 54 | } 55 | 56 | -------------------------------------------------------------------------------- /community/models/subscriptions.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import Model from require "community.model" 3 | 4 | import insert_on_conflict_ignore from require "community.helpers.models" 5 | 6 | -- Generated schema dump: (do not edit) 7 | -- 8 | -- CREATE TABLE community_subscriptions ( 9 | -- object_type smallint NOT NULL, 10 | -- object_id integer NOT NULL, 11 | -- user_id integer NOT NULL, 12 | -- subscribed boolean DEFAULT true NOT NULL, 13 | -- created_at timestamp without time zone NOT NULL, 14 | -- updated_at timestamp without time zone NOT NULL 15 | -- ); 16 | -- ALTER TABLE ONLY community_subscriptions 17 | -- ADD CONSTRAINT community_subscriptions_pkey PRIMARY KEY (object_type, object_id, user_id); 18 | -- CREATE INDEX community_subscriptions_user_id_idx ON community_subscriptions USING btree (user_id); 19 | -- 20 | class Subscriptions extends Model 21 | @primary_key: {"object_type", "object_id", "user_id"} 22 | 23 | @timestamp: true 24 | 25 | @relations: { 26 | {"user", belongs_to: "Users"} 27 | {"object", polymorphic_belongs_to: { 28 | [1]: {"topic", "Topics"} 29 | [2]: {"category", "Categories"} 30 | }} 31 | } 32 | 33 | @create: (opts={}) => 34 | if opts.object_type 35 | opts.object_type = @object_types\for_db opts.object_type 36 | 37 | insert_on_conflict_ignore @, opts 38 | 39 | @find_subscription: (object, user) => 40 | return nil unless user 41 | 42 | @find { 43 | user_id: user.id 44 | object_type: @object_type_for_object object 45 | object_id: object.id 46 | } 47 | 48 | @is_subscribed: (object, user, subscribed_by_default=false) => 49 | return unless user 50 | 51 | sub = @find_subscription object, user 52 | if subscribed_by_default 53 | not sub or sub.subscribed 54 | else 55 | sub and sub.subscribed 56 | 57 | @subscribe: (object, user, subscribed_by_default=false) => 58 | return unless user 59 | 60 | sub = @find_subscription object, user 61 | 62 | if subscribed_by_default 63 | if sub 64 | sub\delete! 65 | return true 66 | else 67 | return 68 | 69 | return if sub and sub.subscribed 70 | 71 | if sub 72 | sub\update subscribed: true 73 | else 74 | @create { 75 | user_id: user.id 76 | object_type: @object_type_for_object object 77 | object_id: object.id 78 | } 79 | 80 | true 81 | 82 | @unsubscribe: (object, user, subscribed_by_default=false) => 83 | return unless user 84 | sub = @find_subscription object, user 85 | 86 | if subscribed_by_default 87 | if sub 88 | return unless sub.subscribed 89 | sub\update subscribed: false 90 | else 91 | @create { 92 | user_id: user.id 93 | object_type: @object_type_for_object object 94 | object_id: object.id 95 | subscribed: false 96 | } 97 | true 98 | else 99 | if sub 100 | sub\delete! 101 | return true 102 | 103 | 104 | is_subscribed: => 105 | @subscribed 106 | -------------------------------------------------------------------------------- /community/models/topic_participants.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local TopicParticipants 5 | do 6 | local _class_0 7 | local _parent_0 = Model 8 | local _base_0 = { } 9 | _base_0.__index = _base_0 10 | setmetatable(_base_0, _parent_0.__base) 11 | _class_0 = setmetatable({ 12 | __init = function(self, ...) 13 | return _class_0.__parent.__init(self, ...) 14 | end, 15 | __base = _base_0, 16 | __name = "TopicParticipants", 17 | __parent = _parent_0 18 | }, { 19 | __index = function(cls, name) 20 | local val = rawget(_base_0, name) 21 | if val == nil then 22 | local parent = rawget(cls, "__parent") 23 | if parent then 24 | return parent[name] 25 | end 26 | else 27 | return val 28 | end 29 | end, 30 | __call = function(cls, ...) 31 | local _self_0 = setmetatable({}, _base_0) 32 | cls.__init(_self_0, ...) 33 | return _self_0 34 | end 35 | }) 36 | _base_0.__class = _class_0 37 | local self = _class_0 38 | self.primary_key = { 39 | "topic_id", 40 | "user_id" 41 | } 42 | self.timestamp = true 43 | self.relations = { 44 | { 45 | "user", 46 | belongs_to = "Users" 47 | }, 48 | { 49 | "topic", 50 | belongs_to = "Topics" 51 | } 52 | } 53 | self.increment = function(self, topic_id, user_id) 54 | local insert_on_conflict_update 55 | insert_on_conflict_update = require("community.helpers.models").insert_on_conflict_update 56 | local col = tostring(self:table_name()) .. ".posts_count" 57 | return insert_on_conflict_update(self, { 58 | user_id = user_id, 59 | topic_id = topic_id 60 | }, { 61 | posts_count = 1 62 | }, { 63 | posts_count = db.raw(tostring(col) .. " + 1") 64 | }) 65 | end 66 | self.decrement = function(self, topic_id, user_id) 67 | local key = { 68 | user_id = user_id, 69 | topic_id = topic_id 70 | } 71 | local res = db.update(self:table_name(), { 72 | posts_count = db.raw("posts_count - 1") 73 | }, key, "posts_count") 74 | if res[1] and res[1].posts_count == 0 then 75 | key.posts_count = 0 76 | return db.delete(self:table_name(), key) 77 | end 78 | end 79 | if _parent_0.__inherited then 80 | _parent_0.__inherited(_parent_0, _class_0) 81 | end 82 | TopicParticipants = _class_0 83 | return _class_0 84 | end 85 | -------------------------------------------------------------------------------- /community/models/topic_participants.moon: -------------------------------------------------------------------------------- 1 | 2 | db = require "lapis.db" 3 | import Model from require "community.model" 4 | 5 | -- Generated schema dump: (do not edit) 6 | -- 7 | -- CREATE TABLE community_topic_participants ( 8 | -- topic_id integer NOT NULL, 9 | -- user_id integer NOT NULL, 10 | -- posts_count integer DEFAULT 0 NOT NULL, 11 | -- created_at timestamp without time zone NOT NULL, 12 | -- updated_at timestamp without time zone NOT NULL 13 | -- ); 14 | -- ALTER TABLE ONLY community_topic_participants 15 | -- ADD CONSTRAINT community_topic_participants_pkey PRIMARY KEY (topic_id, user_id); 16 | -- 17 | class TopicParticipants extends Model 18 | @primary_key: {"topic_id", "user_id"} 19 | @timestamp: true 20 | 21 | @relations: { 22 | {"user", belongs_to: "Users"} 23 | {"topic", belongs_to: "Topics"} 24 | } 25 | 26 | @increment: (topic_id, user_id) => 27 | import insert_on_conflict_update from require "community.helpers.models" 28 | 29 | col = "#{@table_name!}.posts_count" 30 | 31 | insert_on_conflict_update @, { 32 | :user_id, :topic_id 33 | }, { 34 | posts_count: 1 35 | }, { 36 | posts_count: db.raw "#{col} + 1" 37 | } 38 | 39 | @decrement: (topic_id, user_id) => 40 | key = {:user_id, :topic_id} 41 | 42 | res = db.update @table_name!, { 43 | posts_count: db.raw "posts_count - 1" 44 | }, key, "posts_count" 45 | 46 | if res[1] and res[1].posts_count == 0 47 | key.posts_count = 0 48 | db.delete @table_name!, key 49 | -------------------------------------------------------------------------------- /community/models/topic_polls.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local date = require("date") 3 | local enum 4 | enum = require("lapis.db.model").enum 5 | local Model 6 | Model = require("community.model").Model 7 | local TopicPolls 8 | do 9 | local _class_0 10 | local _parent_0 = Model 11 | local _base_0 = { 12 | delete = function(self) 13 | if _class_0.__parent.__base.delete(self) then 14 | local _list_0 = self:get_poll_choices() 15 | for _index_0 = 1, #_list_0 do 16 | local choice = _list_0[_index_0] 17 | choice:delete() 18 | end 19 | return true 20 | end 21 | end, 22 | name_for_display = function(self) 23 | return self.poll_question 24 | end, 25 | allowed_to_edit = function(self, user) 26 | return self:get_topic():allowed_to_edit(user) 27 | end, 28 | allowed_to_vote = function(self, user) 29 | if not (self:is_open()) then 30 | return nil, "poll is closed" 31 | end 32 | return self:get_topic():allowed_to_view(user) 33 | end, 34 | is_open = function(self) 35 | local now = date(true) 36 | return now >= date(self.start_date) and now < date(self.end_date) 37 | end, 38 | total_vote_count = function(self) 39 | local sum = 0 40 | local _list_0 = self:get_poll_choices() 41 | for _index_0 = 1, #_list_0 do 42 | local choice = _list_0[_index_0] 43 | sum = sum + choice.vote_count 44 | end 45 | return sum 46 | end 47 | } 48 | _base_0.__index = _base_0 49 | setmetatable(_base_0, _parent_0.__base) 50 | _class_0 = setmetatable({ 51 | __init = function(self, ...) 52 | return _class_0.__parent.__init(self, ...) 53 | end, 54 | __base = _base_0, 55 | __name = "TopicPolls", 56 | __parent = _parent_0 57 | }, { 58 | __index = function(cls, name) 59 | local val = rawget(_base_0, name) 60 | if val == nil then 61 | local parent = rawget(cls, "__parent") 62 | if parent then 63 | return parent[name] 64 | end 65 | else 66 | return val 67 | end 68 | end, 69 | __call = function(cls, ...) 70 | local _self_0 = setmetatable({}, _base_0) 71 | cls.__init(_self_0, ...) 72 | return _self_0 73 | end 74 | }) 75 | _base_0.__class = _class_0 76 | local self = _class_0 77 | self.timestamp = true 78 | self.relations = { 79 | { 80 | "topic", 81 | belongs_to = "Topics" 82 | }, 83 | { 84 | "poll_choices", 85 | has_many = "PollChoices", 86 | key = "poll_id", 87 | order = "position ASC" 88 | } 89 | } 90 | self.vote_types = enum({ 91 | single = 1, 92 | multiple = 2 93 | }) 94 | self.create = function(self, opts) 95 | if opts == nil then 96 | opts = { } 97 | end 98 | opts.vote_type = self.vote_types:for_db(opts.vote_type or "single") 99 | return _class_0.__parent.create(self, opts) 100 | end 101 | if _parent_0.__inherited then 102 | _parent_0.__inherited(_parent_0, _class_0) 103 | end 104 | TopicPolls = _class_0 105 | return _class_0 106 | end 107 | -------------------------------------------------------------------------------- /community/models/topic_polls.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | 3 | date = require "date" 4 | 5 | import enum from require "lapis.db.model" 6 | 7 | import Model from require "community.model" 8 | 9 | -- Generated schema dump: (do not edit) 10 | -- 11 | -- CREATE TABLE community_topic_polls ( 12 | -- id integer NOT NULL, 13 | -- topic_id integer NOT NULL, 14 | -- poll_question text NOT NULL, 15 | -- description text, 16 | -- vote_type smallint NOT NULL, 17 | -- anonymous boolean DEFAULT true NOT NULL, 18 | -- hide_results boolean DEFAULT false NOT NULL, 19 | -- start_date timestamp without time zone DEFAULT date_trunc('second'::text, (now() AT TIME ZONE 'utc'::text)) NOT NULL, 20 | -- end_date timestamp without time zone NOT NULL, 21 | -- created_at timestamp without time zone NOT NULL, 22 | -- updated_at timestamp without time zone NOT NULL 23 | -- ); 24 | -- ALTER TABLE ONLY community_topic_polls 25 | -- ADD CONSTRAINT community_topic_polls_pkey PRIMARY KEY (id); 26 | -- CREATE UNIQUE INDEX community_topic_polls_topic_id_idx ON community_topic_polls USING btree (topic_id); 27 | -- 28 | class TopicPolls extends Model 29 | @timestamp: true 30 | 31 | @relations: { 32 | {"topic", belongs_to: "Topics"} 33 | {"poll_choices", has_many: "PollChoices", key: "poll_id", order: "position ASC"} 34 | } 35 | 36 | @vote_types: enum { 37 | single: 1 -- user can vote on a single choice 38 | multiple: 2 -- user can vote on any number of choices 39 | } 40 | 41 | @create: (opts={}) => 42 | opts.vote_type = @vote_types\for_db opts.vote_type or "single" 43 | super opts 44 | 45 | delete: => 46 | if super! 47 | -- clean up poll choices and votes 48 | for choice in *@get_poll_choices! 49 | choice\delete! 50 | true 51 | 52 | name_for_display: => 53 | @poll_question 54 | 55 | allowed_to_edit: (user) => 56 | @get_topic!\allowed_to_edit user 57 | 58 | allowed_to_vote: (user) => 59 | unless @is_open! 60 | return nil, "poll is closed" 61 | 62 | @get_topic!\allowed_to_view user 63 | 64 | is_open: => 65 | now = date(true) 66 | now >= date(@start_date) and now < date(@end_date) 67 | 68 | total_vote_count: => 69 | sum = 0 70 | for choice in *@get_poll_choices! 71 | sum += choice.vote_count 72 | 73 | sum 74 | 75 | -------------------------------------------------------------------------------- /community/models/topic_subscriptions.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local insert_on_conflict_ignore 5 | insert_on_conflict_ignore = require("community.helpers.models").insert_on_conflict_ignore 6 | local TopicSubscriptions 7 | do 8 | local _class_0 9 | local _parent_0 = Model 10 | local _base_0 = { } 11 | _base_0.__index = _base_0 12 | setmetatable(_base_0, _parent_0.__base) 13 | _class_0 = setmetatable({ 14 | __init = function(self, ...) 15 | return _class_0.__parent.__init(self, ...) 16 | end, 17 | __base = _base_0, 18 | __name = "TopicSubscriptions", 19 | __parent = _parent_0 20 | }, { 21 | __index = function(cls, name) 22 | local val = rawget(_base_0, name) 23 | if val == nil then 24 | local parent = rawget(cls, "__parent") 25 | if parent then 26 | return parent[name] 27 | end 28 | else 29 | return val 30 | end 31 | end, 32 | __call = function(cls, ...) 33 | local _self_0 = setmetatable({}, _base_0) 34 | cls.__init(_self_0, ...) 35 | return _self_0 36 | end 37 | }) 38 | _base_0.__class = _class_0 39 | local self = _class_0 40 | self.primary_key = { 41 | "topic_id", 42 | "user_id" 43 | } 44 | self.timestamp = true 45 | self.relations = { 46 | { 47 | "user", 48 | belongs_to = "Users" 49 | }, 50 | { 51 | "topic", 52 | belongs_to = "Topics" 53 | } 54 | } 55 | self.create = insert_on_conflict_ignore 56 | if _parent_0.__inherited then 57 | _parent_0.__inherited(_parent_0, _class_0) 58 | end 59 | TopicSubscriptions = _class_0 60 | return _class_0 61 | end 62 | -------------------------------------------------------------------------------- /community/models/user_category_last_seens.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local UserCategoryLastSeens 5 | do 6 | local _class_0 7 | local _parent_0 = Model 8 | local _base_0 = { 9 | should_update = function(self) 10 | local category = self:get_category() 11 | return self.category_order < category:get_last_topic().category_order 12 | end 13 | } 14 | _base_0.__index = _base_0 15 | setmetatable(_base_0, _parent_0.__base) 16 | _class_0 = setmetatable({ 17 | __init = function(self, ...) 18 | return _class_0.__parent.__init(self, ...) 19 | end, 20 | __base = _base_0, 21 | __name = "UserCategoryLastSeens", 22 | __parent = _parent_0 23 | }, { 24 | __index = function(cls, name) 25 | local val = rawget(_base_0, name) 26 | if val == nil then 27 | local parent = rawget(cls, "__parent") 28 | if parent then 29 | return parent[name] 30 | end 31 | else 32 | return val 33 | end 34 | end, 35 | __call = function(cls, ...) 36 | local _self_0 = setmetatable({}, _base_0) 37 | cls.__init(_self_0, ...) 38 | return _self_0 39 | end 40 | }) 41 | _base_0.__class = _class_0 42 | local self = _class_0 43 | self.primary_key = { 44 | "user_id", 45 | "category_id" 46 | } 47 | self.relations = { 48 | { 49 | "user", 50 | belongs_to = "Users" 51 | }, 52 | { 53 | "category", 54 | belongs_to = "Categories" 55 | }, 56 | { 57 | "topic", 58 | belongs_to = "Topics" 59 | } 60 | } 61 | if _parent_0.__inherited then 62 | _parent_0.__inherited(_parent_0, _class_0) 63 | end 64 | UserCategoryLastSeens = _class_0 65 | return _class_0 66 | end 67 | -------------------------------------------------------------------------------- /community/models/user_category_last_seens.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import Model from require "community.model" 3 | 4 | -- Generated schema dump: (do not edit) 5 | -- 6 | -- CREATE TABLE community_user_category_last_seens ( 7 | -- user_id integer NOT NULL, 8 | -- category_id integer NOT NULL, 9 | -- category_order integer DEFAULT 0 NOT NULL, 10 | -- topic_id integer NOT NULL 11 | -- ); 12 | -- ALTER TABLE ONLY community_user_category_last_seens 13 | -- ADD CONSTRAINT community_user_category_last_seens_pkey PRIMARY KEY (user_id, category_id); 14 | -- 15 | class UserCategoryLastSeens extends Model 16 | @primary_key: { "user_id", "category_id" } 17 | 18 | @relations: { 19 | {"user", belongs_to: "Users"} 20 | {"category", belongs_to: "Categories"} 21 | {"topic", belongs_to: "Topics"} 22 | } 23 | 24 | should_update: => 25 | category = @get_category! 26 | @category_order < category\get_last_topic!.category_order 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /community/models/user_topic_last_seens.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local Model 3 | Model = require("community.model").Model 4 | local UserTopicLastSeens 5 | do 6 | local _class_0 7 | local _parent_0 = Model 8 | local _base_0 = { } 9 | _base_0.__index = _base_0 10 | setmetatable(_base_0, _parent_0.__base) 11 | _class_0 = setmetatable({ 12 | __init = function(self, ...) 13 | return _class_0.__parent.__init(self, ...) 14 | end, 15 | __base = _base_0, 16 | __name = "UserTopicLastSeens", 17 | __parent = _parent_0 18 | }, { 19 | __index = function(cls, name) 20 | local val = rawget(_base_0, name) 21 | if val == nil then 22 | local parent = rawget(cls, "__parent") 23 | if parent then 24 | return parent[name] 25 | end 26 | else 27 | return val 28 | end 29 | end, 30 | __call = function(cls, ...) 31 | local _self_0 = setmetatable({}, _base_0) 32 | cls.__init(_self_0, ...) 33 | return _self_0 34 | end 35 | }) 36 | _base_0.__class = _class_0 37 | local self = _class_0 38 | self.primary_key = { 39 | "user_id", 40 | "topic_id" 41 | } 42 | self.relations = { 43 | { 44 | "user", 45 | belongs_to = "Users" 46 | }, 47 | { 48 | "topic", 49 | belongs_to = "Topics" 50 | }, 51 | { 52 | "post", 53 | belongs_to = "Posts" 54 | } 55 | } 56 | if _parent_0.__inherited then 57 | _parent_0.__inherited(_parent_0, _class_0) 58 | end 59 | UserTopicLastSeens = _class_0 60 | return _class_0 61 | end 62 | -------------------------------------------------------------------------------- /community/models/user_topic_last_seens.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import Model from require "community.model" 3 | 4 | -- Generated schema dump: (do not edit) 5 | -- 6 | -- CREATE TABLE community_user_topic_last_seens ( 7 | -- user_id integer NOT NULL, 8 | -- topic_id integer NOT NULL, 9 | -- post_id integer NOT NULL 10 | -- ); 11 | -- ALTER TABLE ONLY community_user_topic_last_seens 12 | -- ADD CONSTRAINT community_user_topic_last_seens_pkey PRIMARY KEY (user_id, topic_id); 13 | -- CREATE INDEX community_user_topic_last_seens_topic_id_idx ON community_user_topic_last_seens USING btree (topic_id); 14 | -- 15 | class UserTopicLastSeens extends Model 16 | @primary_key: {"user_id", "topic_id"} 17 | 18 | @relations: { 19 | {"user", belongs_to: "Users"} 20 | {"topic", belongs_to: "Topics"} 21 | {"post", belongs_to: "Posts"} 22 | } 23 | 24 | -------------------------------------------------------------------------------- /community/models/virtual/user_users.lua: -------------------------------------------------------------------------------- 1 | local VirtualModel 2 | VirtualModel = require("community.model").VirtualModel 3 | local UserUsers 4 | do 5 | local _class_0 6 | local _parent_0 = VirtualModel 7 | local _base_0 = { } 8 | _base_0.__index = _base_0 9 | setmetatable(_base_0, _parent_0.__base) 10 | _class_0 = setmetatable({ 11 | __init = function(self, ...) 12 | return _class_0.__parent.__init(self, ...) 13 | end, 14 | __base = _base_0, 15 | __name = "UserUsers", 16 | __parent = _parent_0 17 | }, { 18 | __index = function(cls, name) 19 | local val = rawget(_base_0, name) 20 | if val == nil then 21 | local parent = rawget(cls, "__parent") 22 | if parent then 23 | return parent[name] 24 | end 25 | else 26 | return val 27 | end 28 | end, 29 | __call = function(cls, ...) 30 | local _self_0 = setmetatable({}, _base_0) 31 | cls.__init(_self_0, ...) 32 | return _self_0 33 | end 34 | }) 35 | _base_0.__class = _class_0 36 | local self = _class_0 37 | self.primary_key = { 38 | "source_user_id", 39 | "dest_user_id" 40 | } 41 | self.relations = { 42 | { 43 | "block_given", 44 | has_one = "Blocks", 45 | key = { 46 | blocking_user_id = "source_user_id", 47 | blocked_user_id = "dest_user_id" 48 | } 49 | }, 50 | { 51 | "block_recieved", 52 | has_one = "Blocks", 53 | key = { 54 | blocking_user_id = "dest_user_id", 55 | blocked_user_id = "source_user_id" 56 | } 57 | } 58 | } 59 | if _parent_0.__inherited then 60 | _parent_0.__inherited(_parent_0, _class_0) 61 | end 62 | UserUsers = _class_0 63 | return _class_0 64 | end 65 | -------------------------------------------------------------------------------- /community/models/virtual/user_users.moon: -------------------------------------------------------------------------------- 1 | import VirtualModel from require "community.model" 2 | 3 | -- a user's relationship with another user 4 | class UserUsers extends VirtualModel 5 | @primary_key: {"source_user_id", "dest_user_id"} 6 | 7 | @relations: { 8 | {"block_given", has_one: "Blocks", key: { 9 | blocking_user_id: "source_user_id" 10 | blocked_user_id: "dest_user_id" 11 | }} 12 | 13 | {"block_recieved", has_one: "Blocks", key: { 14 | blocking_user_id: "dest_user_id" 15 | blocked_user_id: "source_user_id" 16 | }} 17 | } 18 | -------------------------------------------------------------------------------- /community/models/warnings.lua: -------------------------------------------------------------------------------- 1 | local db = require("lapis.db") 2 | local enum 3 | enum = require("lapis.db.model").enum 4 | local Model 5 | Model = require("community.model").Model 6 | local db_json 7 | db_json = require("community.helpers.models").db_json 8 | local Warnings 9 | do 10 | local _class_0 11 | local _parent_0 = Model 12 | local _base_0 = { 13 | is_active = function(self) 14 | local date = require("date") 15 | return not self.expires_at or date(true) < date(self.expires_at) 16 | end, 17 | has_started = function(self) 18 | return self.first_seen_at ~= nil 19 | end, 20 | start_warning = function(self) 21 | return self:update({ 22 | first_seen_at = db.raw("date_trunc('second', now() at time zone 'UTC')"), 23 | expires_at = db.raw([[date_trunc('second', now() at time zone 'UTC') + duration]]) 24 | }, { 25 | where = db.clause({ 26 | first_seen_at = db.NULL 27 | }) 28 | }) 29 | end, 30 | end_warning = function(self) 31 | return self:update({ 32 | first_seen_at = db.raw("coalesce(first_seen_at, date_trunc('second', now() at time zone 'UTC'))"), 33 | expires_at = db.raw([[date_trunc('second', now() at time zone 'UTC')]]) 34 | }, { 35 | where = db.clause({ 36 | "expires_at IS NULL or now() at time zone 'utc' < expires_at" 37 | }) 38 | }) 39 | end 40 | } 41 | _base_0.__index = _base_0 42 | setmetatable(_base_0, _parent_0.__base) 43 | _class_0 = setmetatable({ 44 | __init = function(self, ...) 45 | return _class_0.__parent.__init(self, ...) 46 | end, 47 | __base = _base_0, 48 | __name = "Warnings", 49 | __parent = _parent_0 50 | }, { 51 | __index = function(cls, name) 52 | local val = rawget(_base_0, name) 53 | if val == nil then 54 | local parent = rawget(cls, "__parent") 55 | if parent then 56 | return parent[name] 57 | end 58 | else 59 | return val 60 | end 61 | end, 62 | __call = function(cls, ...) 63 | local _self_0 = setmetatable({}, _base_0) 64 | cls.__init(_self_0, ...) 65 | return _self_0 66 | end 67 | }) 68 | _base_0.__class = _class_0 69 | local self = _class_0 70 | self.timestamp = true 71 | self.relations = { 72 | { 73 | "user", 74 | belongs_to = "Users" 75 | }, 76 | { 77 | "moderating_user", 78 | belongs_to = "Users" 79 | }, 80 | { 81 | "post", 82 | belongs_to = "Posts" 83 | }, 84 | { 85 | "post_report", 86 | belongs_to = "PostReports" 87 | } 88 | } 89 | self.restrictions = enum({ 90 | notify = 1, 91 | block_posting = 2, 92 | pending_posting = 3 93 | }) 94 | self.create = function(self, opts, ...) 95 | if opts.restriction then 96 | opts.restriction = self.restrictions:for_db(opts.restriction) 97 | end 98 | if opts.data then 99 | opts.data = db_json(opts.data) 100 | end 101 | return _class_0.__parent.create(self, opts, ...) 102 | end 103 | if _parent_0.__inherited then 104 | _parent_0.__inherited(_parent_0, _class_0) 105 | end 106 | Warnings = _class_0 107 | return _class_0 108 | end 109 | -------------------------------------------------------------------------------- /community/models/warnings.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | import enum from require "lapis.db.model" 3 | import Model from require "community.model" 4 | 5 | import db_json from require "community.helpers.models" 6 | 7 | -- Generated schema dump: (do not edit) 8 | -- 9 | -- CREATE TABLE community_warnings ( 10 | -- id integer NOT NULL, 11 | -- user_id integer NOT NULL, 12 | -- reason text, 13 | -- data jsonb, 14 | -- restriction smallint DEFAULT 1 NOT NULL, 15 | -- duration interval NOT NULL, 16 | -- first_seen_at timestamp without time zone, 17 | -- expires_at timestamp without time zone, 18 | -- moderating_user_id integer, 19 | -- post_id integer, 20 | -- post_report_id integer, 21 | -- created_at timestamp without time zone NOT NULL, 22 | -- updated_at timestamp without time zone NOT NULL 23 | -- ); 24 | -- COMMENT ON COLUMN community_warnings.expires_at IS 'Is set when the user first sees the warning'; 25 | -- ALTER TABLE ONLY community_warnings 26 | -- ADD CONSTRAINT community_warnings_pkey PRIMARY KEY (id); 27 | -- CREATE INDEX community_warnings_user_id_idx ON community_warnings USING btree (user_id); 28 | -- 29 | class Warnings extends Model 30 | @timestamp: true 31 | @relations: { 32 | {"user", belongs_to: "Users"} 33 | {"moderating_user", belongs_to: "Users"} 34 | {"post", belongs_to: "Posts"} 35 | {"post_report", belongs_to: "PostReports"} 36 | } 37 | 38 | @restrictions: enum { 39 | notify: 1 -- user is displayd warning but they can function normally 40 | block_posting: 2 41 | pending_posting: 3 42 | } 43 | 44 | @create: (opts, ...) => 45 | if opts.restriction 46 | opts.restriction = @restrictions\for_db opts.restriction 47 | 48 | if opts.data 49 | opts.data = db_json opts.data 50 | 51 | super opts, ... 52 | 53 | is_active: => 54 | date = require "date" 55 | not @expires_at or date(true) < date(@expires_at) 56 | 57 | has_started: => 58 | @first_seen_at != nil 59 | 60 | -- this should be called the first time the user views the warning to start 61 | -- the restriction for the warning duration 62 | start_warning: => 63 | @update { 64 | first_seen_at: db.raw "date_trunc('second', now() at time zone 'UTC')" 65 | expires_at: db.raw [[date_trunc('second', now() at time zone 'UTC') + duration]] 66 | }, where: db.clause { 67 | first_seen_at: db.NULL 68 | } 69 | 70 | -- immediately end the warning if it's not already over 71 | end_warning: => 72 | @update { 73 | first_seen_at: db.raw "coalesce(first_seen_at, date_trunc('second', now() at time zone 'UTC'))" 74 | expires_at: db.raw [[date_trunc('second', now() at time zone 'UTC')]] 75 | }, where: db.clause { 76 | "expires_at IS NULL or now() at time zone 'utc' < expires_at" 77 | } 78 | -------------------------------------------------------------------------------- /community/schema.lua: -------------------------------------------------------------------------------- 1 | local run_migrations 2 | run_migrations = function(version) 3 | local m = require("lapis.db.migrations") 4 | local migrations = require("community.migrations") 5 | if version and not migrations[tonumber(version)] then 6 | local versions 7 | do 8 | local _accum_0 = { } 9 | local _len_0 = 1 10 | for key in pairs(migrations) do 11 | _accum_0[_len_0] = key 12 | _len_0 = _len_0 + 1 13 | end 14 | versions = _accum_0 15 | end 16 | table.sort(versions) 17 | local available_version = versions[#versions] 18 | error("Expected to migrate to lapis-community version " .. tostring(version) .. " but it was not found (have " .. tostring(available_version) .. "). Did you forget to update lapis-community?") 19 | end 20 | return m.run_migrations(migrations, "community") 21 | end 22 | return { 23 | run_migrations = run_migrations 24 | } 25 | -------------------------------------------------------------------------------- /community/schema.moon: -------------------------------------------------------------------------------- 1 | 2 | run_migrations = (version) -> 3 | m = require "lapis.db.migrations" 4 | 5 | migrations = require("community.migrations") 6 | 7 | if version and not migrations[tonumber version] 8 | versions = [key for key in pairs migrations] 9 | table.sort versions 10 | available_version = versions[#versions] 11 | error "Expected to migrate to lapis-community version #{version} but it was not found (have #{available_version}). Did you forget to update lapis-community?" 12 | 13 | m.run_migrations migrations, "community" 14 | 15 | { :run_migrations } 16 | -------------------------------------------------------------------------------- /community/version.lua: -------------------------------------------------------------------------------- 1 | return "1.44.3" 2 | -------------------------------------------------------------------------------- /community/version.moon: -------------------------------------------------------------------------------- 1 | "1.44.3" 2 | -------------------------------------------------------------------------------- /config.moon: -------------------------------------------------------------------------------- 1 | config = require "lapis.config" 2 | 3 | config "development", -> 4 | measure_performance true 5 | 6 | postgres { 7 | backend: "pgmoon" 8 | database: "community" 9 | } 10 | 11 | community { 12 | view_counter_dict: "view_counters" 13 | } 14 | 15 | config "test", -> 16 | measure_performance true 17 | logging { 18 | requests: true 19 | queries: false 20 | server: true 21 | } 22 | 23 | postgres { 24 | backend: "pgmoon" 25 | database: "community_test" 26 | 27 | host: os.getenv "PGHOST" 28 | user: os.getenv "PGUSER" 29 | password: os.getenv "PGPASSWORD" 30 | } 31 | 32 | 33 | -------------------------------------------------------------------------------- /lint_config.moon: -------------------------------------------------------------------------------- 1 | { 2 | whitelist_globals: { 3 | ["."]: { "ngx" } 4 | 5 | ["spec/"]: { 6 | "it", "describe", "before_each", "after_each", "setup", "teardown", "stub", "spy" 7 | } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /mime.types: -------------------------------------------------------------------------------- 1 | types { 2 | text/html html htm shtml; 3 | text/css css; 4 | text/xml xml; 5 | image/gif gif; 6 | image/jpeg jpeg jpg; 7 | application/x-javascript js; 8 | application/atom+xml atom; 9 | application/rss+xml rss; 10 | 11 | text/mathml mml; 12 | text/plain txt; 13 | text/vnd.sun.j2me.app-descriptor jad; 14 | text/vnd.wap.wml wml; 15 | text/x-component htc; 16 | 17 | image/png png; 18 | image/tiff tif tiff; 19 | image/vnd.wap.wbmp wbmp; 20 | image/x-icon ico; 21 | image/x-jng jng; 22 | image/x-ms-bmp bmp; 23 | image/svg+xml svg svgz; 24 | image/webp webp; 25 | 26 | application/java-archive jar war ear; 27 | application/mac-binhex40 hqx; 28 | application/msword doc; 29 | application/pdf pdf; 30 | application/postscript ps eps ai; 31 | application/rtf rtf; 32 | application/vnd.ms-excel xls; 33 | application/vnd.ms-powerpoint ppt; 34 | application/vnd.wap.wmlc wmlc; 35 | application/vnd.google-earth.kml+xml kml; 36 | application/vnd.google-earth.kmz kmz; 37 | application/x-7z-compressed 7z; 38 | application/x-cocoa cco; 39 | application/x-java-archive-diff jardiff; 40 | application/x-java-jnlp-file jnlp; 41 | application/x-makeself run; 42 | application/x-perl pl pm; 43 | application/x-pilot prc pdb; 44 | application/x-rar-compressed rar; 45 | application/x-redhat-package-manager rpm; 46 | application/x-sea sea; 47 | application/x-shockwave-flash swf; 48 | application/x-stuffit sit; 49 | application/x-tcl tcl tk; 50 | application/x-x509-ca-cert der pem crt; 51 | application/x-xpinstall xpi; 52 | application/xhtml+xml xhtml; 53 | application/zip zip; 54 | 55 | application/octet-stream bin exe dll; 56 | application/octet-stream deb; 57 | application/octet-stream dmg; 58 | application/octet-stream eot; 59 | application/octet-stream iso img; 60 | application/octet-stream msi msp msm; 61 | 62 | audio/midi mid midi kar; 63 | audio/mpeg mp3; 64 | audio/ogg ogg; 65 | audio/x-m4a m4a; 66 | audio/x-realaudio ra; 67 | 68 | video/3gpp 3gpp 3gp; 69 | video/mp4 mp4; 70 | video/mpeg mpeg mpg; 71 | video/quicktime mov; 72 | video/webm webm; 73 | video/x-flv flv; 74 | video/x-m4v m4v; 75 | video/x-mng mng; 76 | video/x-ms-asf asx asf; 77 | video/x-ms-wmv wmv; 78 | video/x-msvideo avi; 79 | } 80 | -------------------------------------------------------------------------------- /models.moon: -------------------------------------------------------------------------------- 1 | import autoload from require "lapis.util" 2 | loader = autoload "models" -- , "community.models" 3 | 4 | setmetatable {}, __index: (name) => 5 | assert loader[name], "failed to find model: #{name}" 6 | 7 | -------------------------------------------------------------------------------- /models/Tupfile: -------------------------------------------------------------------------------- 1 | include_rules 2 | -------------------------------------------------------------------------------- /models/community/Tupfile: -------------------------------------------------------------------------------- 1 | include_rules 2 | -------------------------------------------------------------------------------- /models/users.moon: -------------------------------------------------------------------------------- 1 | 2 | import Model from require "lapis.db.model" 3 | 4 | class Users extends Model 5 | @timestamp: true 6 | is_admin: => false 7 | name_for_display: => @username 8 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes ${{NUM_WORKERS}}; 2 | error_log stderr notice; 3 | daemon off; 4 | 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | include mime.types; 11 | lua_shared_dict view_counters 5m; 12 | 13 | server { 14 | listen ${{PORT}}; 15 | lua_code_cache ${{CODE_CACHE}}; 16 | 17 | location / { 18 | default_type text/html; 19 | content_by_lua ' 20 | require("lapis").serve("app") 21 | '; 22 | } 23 | 24 | location /static/ { 25 | alias static/; 26 | } 27 | 28 | location /favicon.ico { 29 | alias static/favicon.ico; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /schema.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db.postgres" 2 | schema = require "lapis.db.schema" 3 | 4 | import create_table, create_index, drop_table from schema 5 | 6 | make_schema = -> 7 | require("community.schema").run_migrations! 8 | 9 | { 10 | :serial 11 | :varchar 12 | :time 13 | :integer 14 | } = schema.types 15 | 16 | create_table "users", { 17 | {"id", serial} 18 | {"username", varchar} 19 | 20 | {"created_at", time} 21 | {"updated_at", time} 22 | 23 | "PRIMARY KEY (id)" 24 | } 25 | 26 | create_index "users", db.raw"lower(username)", unique: true 27 | 28 | { :make_schema } 29 | -------------------------------------------------------------------------------- /spec/community_models.moon: -------------------------------------------------------------------------------- 1 | -- this will let you import specs and also set up truncation 2 | -- you should call this inside a descibe block 3 | 4 | setmetatable {}, { 5 | __index: (model_name) => 6 | import truncate_tables from require "lapis.spec.db" 7 | import before_each from require "busted" 8 | 9 | with m = assert require("community.models")[model_name], "invalid model: #{model_name}" 10 | before_each -> 11 | truncate_tables m 12 | } 13 | -------------------------------------------------------------------------------- /spec/counters_spec.moon: -------------------------------------------------------------------------------- 1 | import Users from require "models" 2 | import Topics from require "community.models" 3 | 4 | factory = require "spec.factory" 5 | 6 | describe "community.helpers.counters", -> 7 | import Users from require "spec.models" 8 | import Topics from require "spec.community_models" 9 | 10 | it "should bulk increment", -> 11 | t1 = factory.Topics! 12 | t2 = factory.Topics! 13 | t3 = factory.Topics! 14 | 15 | import bulk_increment from require "community.helpers.counters" 16 | 17 | bulk_increment Topics, "views_count", { 18 | {t1.id, 1}, {t2.id, 2} 19 | } 20 | 21 | t1\refresh! 22 | t2\refresh! 23 | t3\refresh! 24 | 25 | assert.same 1, t1.views_count 26 | assert.same 2, t2.views_count 27 | assert.same 0, t3.views_count 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /spec/factory.moon: -------------------------------------------------------------------------------- 1 | factory = require "community.spec.factory" 2 | 3 | base_models = require "models" 4 | 5 | factory.Users = (opts={}) -> 6 | community_user = opts.community_user 7 | opts.community_user = nil 8 | opts.username or= "user-#{factory.next_counter "username"}" 9 | 10 | with user = assert base_models.Users\create opts 11 | if community_user 12 | factory.CommunityUsers user_id: user.id 13 | 14 | factory 15 | -------------------------------------------------------------------------------- /spec/flow_helpers.moon: -------------------------------------------------------------------------------- 1 | import Application from require "lapis.application" 2 | import capture_errors from require "lapis.application" 3 | import mock_action from require "lapis.spec.request" 4 | 5 | return_errors = (fn) -> 6 | capture_errors fn, (req) -> 7 | nil, req.errors 8 | 9 | assert = require "luassert" 10 | 11 | class S extends Application 12 | flows_prefix: "community.flows" 13 | 14 | -- run the / action directly with no routing or error capturing 15 | dispatch: (req, res) => 16 | r = @.Request @, req, res 17 | @wrap_handler(@["/"]) {}, req.parsed_url.path, "index", r 18 | @render_request r 19 | 20 | in_request = (opts, run) -> 21 | assert mock_action S, "/", opts, return_errors run 22 | 23 | -- this creates a proxy object for calling flow methods in the context of a request: 24 | -- flow("categories", user: current_user, post: { category_id: 10 })\recent_posts {} 25 | flow = (flow_name, opts={}) -> 26 | setmetatable { }, { 27 | __index: (proxy, field) -> 28 | flow_cls = require "community.flows.#{flow_name}" 29 | 30 | v = flow_cls.__base[field] 31 | 32 | switch type(v) 33 | when "function" 34 | (_, ...) -> 35 | args = {...} 36 | 37 | in_request { 38 | post: opts.post 39 | get: opts.get 40 | }, => 41 | proxy._last_request = @ 42 | @current_user = opts.user 43 | 44 | if opts.init 45 | if type(opts.init) == "function" 46 | opts.init @ 47 | else 48 | for k,v in pairs opts.init 49 | @[k] = v 50 | 51 | f = flow_cls @ 52 | f[field] f, unpack args 53 | else 54 | v 55 | } 56 | 57 | 58 | {:S, :return_errors, :in_request, :flow} 59 | -------------------------------------------------------------------------------- /spec/flows/blocks_spec.moon: -------------------------------------------------------------------------------- 1 | import in_request from require "spec.flow_helpers" 2 | 3 | factory = require "spec.factory" 4 | 5 | describe "blocks", -> 6 | import Users from require "spec.models" 7 | import Blocks from require "spec.community_models" 8 | 9 | local current_user 10 | 11 | before_each => 12 | current_user = factory.Users! 13 | 14 | block_user = (post) -> 15 | in_request { 16 | :post 17 | }, => 18 | @current_user = current_user 19 | @flow("blocks")\block_user! 20 | 21 | unblock_user = (post) -> 22 | in_request { 23 | :post 24 | }, => 25 | @current_user = current_user 26 | @flow("blocks")\unblock_user! or "noop" 27 | 28 | it "does nothing with incorrect params", -> 29 | assert.has_error( 30 | -> block_user { } 31 | { 32 | message: { 33 | "blocked_user_id: expected database ID integer" 34 | } 35 | } 36 | ) 37 | 38 | it "should block user", -> 39 | other_user = factory.Users! 40 | 41 | assert block_user { 42 | blocked_user_id: other_user.id 43 | } 44 | 45 | blocks = Blocks\select! 46 | assert.same 1, #blocks 47 | block = unpack blocks 48 | assert.same current_user.id, block.blocking_user_id 49 | assert.same other_user.id, block.blocked_user_id 50 | 51 | it "should not error on double block", -> 52 | other_user = factory.Users! 53 | factory.Blocks blocking_user_id: current_user.id, blocked_user_id: other_user.id 54 | 55 | assert block_user { 56 | blocked_user_id: other_user.id 57 | } 58 | 59 | it "should unblock user", -> 60 | other_user = factory.Users! 61 | factory.Blocks { 62 | blocking_user_id: current_user.id 63 | blocked_user_id: other_user.id 64 | } 65 | 66 | factory.Blocks! 67 | 68 | assert unblock_user { 69 | blocked_user_id: other_user.id 70 | } 71 | 72 | assert.same 1, Blocks\count! 73 | assert.same {}, Blocks\select "where blocking_user_id = ?", current_user.id 74 | 75 | it "doesn't error when trying to unblock someone who isn't blocked", -> 76 | other_user = factory.Users! 77 | 78 | -- block on user from different account 79 | factory.Blocks blocked_user_id: other_user.id 80 | 81 | unblock_user { 82 | blocked_user_id: other_user.id 83 | } 84 | 85 | assert.same 1, Blocks\count! 86 | 87 | describe "show blocks", -> 88 | show_blocks = (get) -> 89 | in_request { 90 | :get 91 | }, => 92 | @current_user = current_user 93 | @flow("blocks")\show_blocks! 94 | 95 | it "show sempty blocks", -> 96 | factory.Blocks! -- unrelated block 97 | assert.same {}, show_blocks! 98 | 99 | it "shows blocks when there are some", -> 100 | factory.Blocks! -- unrelated block 101 | for i=1,2 102 | factory.Blocks blocking_user_id: current_user.id 103 | 104 | blocks = show_blocks! 105 | assert.same 2, #blocks 106 | 107 | -------------------------------------------------------------------------------- /spec/flows/bookmarks_spec.moon: -------------------------------------------------------------------------------- 1 | import in_request from require "spec.flow_helpers" 2 | 3 | factory = require "spec.factory" 4 | 5 | describe "flows.bookmarks", -> 6 | import Users from require "spec.models" 7 | import Bookmarks, Topics from require "spec.community_models" 8 | 9 | local current_user 10 | 11 | before_each -> 12 | current_user = factory.Users! 13 | 14 | describe "show", -> 15 | show_topic_bookmarks = (get) -> 16 | in_request { 17 | :get 18 | }, => 19 | @current_user = current_user 20 | @flow("bookmarks")\show_topic_bookmarks! 21 | @topics 22 | 23 | it "fetches empty topic list", -> 24 | assert.same {}, show_topic_bookmarks! 25 | 26 | it "fetches topic with bookmark", -> 27 | other_topic = factory.Topics! 28 | Bookmarks\save other_topic, factory.Users! 29 | 30 | topics = for i=1,2 31 | with topic = factory.Topics! 32 | Bookmarks\save topic, current_user 33 | 34 | fetched = show_topic_bookmarks! 35 | assert.same {t.id, true for t in *topics}, 36 | {t.id, true for t in *fetched} 37 | 38 | describe "save", -> 39 | save_bookmark = (post={}) -> 40 | in_request { :post }, => 41 | @current_user = current_user 42 | @flow("bookmarks")\save_bookmark! or "noop" 43 | 44 | it "should save a bookmark", -> 45 | topic = factory.Topics! 46 | 47 | assert save_bookmark { 48 | object_type: "topic" 49 | object_id: topic.id 50 | } 51 | 52 | assert.same 1, Bookmarks\count! 53 | bookmark = unpack Bookmarks\select! 54 | assert.same Bookmarks.object_types.topic, bookmark.object_type 55 | assert.same topic.id, bookmark.object_id 56 | assert.same current_user.id, bookmark.user_id 57 | 58 | it "should not error if bookmark exists", -> 59 | topic = factory.Topics! 60 | for i=1,2 61 | save_bookmark { 62 | object_type: "topic" 63 | object_id: topic.id 64 | } 65 | 66 | assert.same 1, Bookmarks\count! 67 | 68 | describe "remove", -> 69 | remove_bookmark = (post={}) -> 70 | in_request { :post }, => 71 | @current_user = current_user 72 | @flow("bookmarks")\remove_bookmark! or "noop" 73 | 74 | it "removes bookmark", -> 75 | bm = factory.Bookmarks user_id: current_user.id 76 | 77 | remove_bookmark { 78 | object_type: "topic" 79 | object_id: bm.object_id 80 | } 81 | 82 | assert.same 0, Bookmarks\count! 83 | 84 | it "handles removing non-existant bookmark", -> 85 | other_bm = factory.Bookmarks! 86 | 87 | remove_bookmark { 88 | object_type: "topic" 89 | object_id: other_bm.object_id 90 | } 91 | 92 | assert.same 1, Bookmarks\count! 93 | 94 | -------------------------------------------------------------------------------- /spec/flows/category_groups_spec.moon: -------------------------------------------------------------------------------- 1 | import in_request from require "spec.flow_helpers" 2 | 3 | factory = require "spec.factory" 4 | 5 | describe "category groups flow", -> 6 | local current_user 7 | 8 | import Users from require "spec.models" 9 | import Categories, CategoryGroups, 10 | CategoryGroupCategories from require "spec.community_models" 11 | 12 | before_each -> 13 | current_user = factory.Users! 14 | 15 | show_categories = (get) -> 16 | in_request { :get }, => 17 | @current_user = current_user 18 | @flow("category_groups")\show_categories! 19 | 20 | new_category_group = (post) -> 21 | in_request { :post }, => 22 | @current_user = current_user 23 | @flow("category_groups")\new_category_group! 24 | 25 | edit_category_group = (post) -> 26 | in_request { :post }, => 27 | @current_user = current_user 28 | @flow("category_groups")\edit_category_group! 29 | 30 | it "shows categories", -> 31 | group = factory.CategoryGroups! 32 | group\add_category factory.Categories! 33 | 34 | categories = show_categories { 35 | category_group_id: group.id 36 | } 37 | 38 | assert.same 1, #categories 39 | 40 | it "creates new category group", -> 41 | new_category_group { 42 | "category_group[title]": "" 43 | } 44 | 45 | assert.same 1, #CategoryGroups\select! 46 | 47 | it "edits category group", -> 48 | group = factory.CategoryGroups { 49 | user_id: current_user.id 50 | description: "yeah" 51 | } 52 | 53 | edit_category_group { 54 | category_group_id: group.id 55 | "category_group[rules]": "follow the rules!" 56 | } 57 | 58 | assert.same 1, #CategoryGroups\select! 59 | 60 | group\refresh! 61 | 62 | assert.same "follow the rules!", group.rules 63 | assert.falsy group.description 64 | 65 | -------------------------------------------------------------------------------- /spec/flows/pending_posts_spec.moon: -------------------------------------------------------------------------------- 1 | import in_request from require "spec.flow_helpers" 2 | 3 | factory = require "spec.factory" 4 | 5 | import types from require "tableshape" 6 | 7 | import instance_of from require "tableshape.moonscript" 8 | 9 | describe "reports", -> 10 | local current_user 11 | 12 | import Users from require "spec.models" 13 | import PendingPosts, Topics, Posts, ActivityLogs from require "spec.community_models" 14 | 15 | before_each -> 16 | current_user = factory.Users! 17 | 18 | it "deletes", -> 19 | pending_post = factory.PendingPosts! 20 | PendingPostsFlow = require "community.flows.pending_posts" 21 | in_request {}, => 22 | @current_user = current_user 23 | PendingPostsFlow(@)\delete_pending_post pending_post 24 | 25 | it "promotes", -> 26 | pending_post = factory.PendingPosts! 27 | PendingPostsFlow = require "community.flows.pending_posts" 28 | post = in_request {}, => 29 | @current_user = current_user 30 | PendingPostsFlow(@)\promote_pending_post pending_post 31 | 32 | assert instance_of(Posts) post 33 | 34 | assert types.shape({ 35 | types.partial { 36 | user_id: current_user.id 37 | action: ActivityLogs.actions.pending_post.promote 38 | object_type: ActivityLogs.object_types.pending_post 39 | object_id: pending_post.id 40 | data: types.partial { 41 | post_id: post.id 42 | } 43 | } 44 | }) ActivityLogs\select! 45 | 46 | -------------------------------------------------------------------------------- /spec/helpers.moon: -------------------------------------------------------------------------------- 1 | 2 | assert = require "luassert" 3 | stub = require "luassert.stub" 4 | 5 | capture_queries = (fn) -> 6 | snapshot = assert\snapshot! 7 | 8 | logger = require "lapis.logging" 9 | 10 | query_log = {} 11 | original = logger.query 12 | stub(logger, "query").invokes (query, ...) -> 13 | table.insert query_log, query 14 | original query, ... 15 | 16 | fn! 17 | 18 | snapshot\revert! 19 | 20 | query_log 21 | 22 | assert_queries = (queries, fn) -> 23 | query_log = capture_queries fn 24 | 25 | msg = if not next queries 26 | "expected no queries" 27 | else 28 | "expected queries to match" 29 | 30 | assert.same queries, query_log, msg 31 | 32 | -- this checks if queries are a subset of the actual queries 33 | assert_has_queries = (queries, fn) -> 34 | query_log = capture_queries fn 35 | 36 | missing_queries = for q in *queries 37 | found = false 38 | for logged_q in *query_log 39 | found = logged_q == q 40 | break if found 41 | 42 | continue if found 43 | q 44 | 45 | assert.same {}, missing_queries, "following queries are missing" 46 | 47 | assert_no_queries = (fn) -> 48 | assert_queries {}, fn 49 | 50 | -- note: we can't do stub(_G, "pairs") because of a limitation of busted 51 | sorted_pairs = (sort=table.sort) -> 52 | import before_each, after_each from require "busted" 53 | local _pairs 54 | before_each -> 55 | _pairs = _G.pairs 56 | _G.pairs = (object, ...) -> 57 | keys = [k for k in _pairs object] 58 | sort keys, (a,b) -> 59 | if type(a) == type(b) 60 | tostring(a) < tostring(b) 61 | else 62 | type(a) < type(b) 63 | 64 | idx = 0 65 | 66 | -> 67 | idx += 1 68 | key = keys[idx] 69 | if key != nil 70 | key, object[key] 71 | 72 | after_each -> 73 | _G.pairs = _pairs 74 | 75 | {:assert_queries, :assert_has_queries, :assert_no_queries, :sorted_pairs, :capture_queries} 76 | -------------------------------------------------------------------------------- /spec/helpers_spec.moon: -------------------------------------------------------------------------------- 1 | describe "community.helpers", -> 2 | 3 | describe "models", -> 4 | import memoize1 from require "community.helpers.models" 5 | 6 | it "memoizes method", -> 7 | class M 8 | calls: 0 9 | 10 | new: (@initial) => 11 | 12 | value: memoize1 (t) => 13 | @calls += 1 14 | @initial + t.amount 15 | 16 | a = M 2 17 | b = M 3 18 | 19 | i1 = amount: 2 20 | i2 = amount: 3 21 | 22 | assert.same 4, a\value i1 23 | assert.same 5, a\value i2 24 | assert.same 4, a\value i1 25 | 26 | assert.same 2, a.calls 27 | 28 | assert.same 5, b\value i1 29 | assert.same 6, b\value i2 30 | assert.same 5, b\value i1 31 | 32 | assert.same 2, b.calls 33 | 34 | 35 | describe "shapes", -> 36 | describe "convert_array", -> 37 | local convert_array 38 | 39 | before_each -> 40 | import convert_array from require "community.helpers.shapes" 41 | 42 | it "converts table", -> 43 | input = { 44 | "1": "hello" 45 | "2": "world" 46 | "3": "zone" 47 | "999": "zone" 48 | } 49 | 50 | assert.same { 51 | "hello", "world", "zone" 52 | }, convert_array\transform input 53 | 54 | it "empty table", -> 55 | assert.same {}, convert_array\transform {} 56 | 57 | it "table with no array", -> 58 | input = { 59 | hello: "world" 60 | thing: { "one", "two" } 61 | } 62 | 63 | assert.same {}, convert_array\transform input 64 | 65 | it "converts existing array", -> 66 | assert.same { 67 | "one", "two" 68 | }, convert_array\transform {"one", "two"} 69 | 70 | assert.same { 71 | {"first"}, {"second"} 72 | }, convert_array\transform { 73 | {"first"}, {"second"} 74 | picker: "true" 75 | } 76 | 77 | 78 | describe "page_number", -> 79 | local page_number 80 | 81 | before_each -> 82 | import page_number from require "community.helpers.shapes" 83 | 84 | it "passes valid value", -> 85 | assert.same 1, page_number\transform "1" 86 | assert.same 200, page_number\transform "200" 87 | assert.same nil, (page_number\transform " 5 ") 88 | 89 | assert.same 1, page_number\transform 1 90 | assert.same 50, page_number\transform 50 91 | assert.same nil, (page_number\transform -20) 92 | assert.same 3, page_number\transform 3.5 93 | 94 | assert.same 1, page_number\transform nil 95 | assert.same 1, page_number\transform "" 96 | 97 | it "fails invalid string", -> 98 | assert.same {nil, "expected empty, or page number"}, {page_number\transform "hello"} 99 | assert.same {nil, "expected empty, or page number"}, {page_number\transform "nil"} 100 | assert.same {nil, "expected empty, or page number"}, {page_number\transform " 5 f"} 101 | assert.same {nil, "expected empty, or page number"}, {page_number\transform "-5"} 102 | assert.same {nil, "expected empty, or page number"}, {page_number\transform "5.3"} 103 | 104 | -------------------------------------------------------------------------------- /spec/models.moon: -------------------------------------------------------------------------------- 1 | -- this will let you import specs and also set up truncation 2 | -- you should call this inside a descibe block 3 | 4 | setmetatable {}, { 5 | __index: (model_name) => 6 | import truncate_tables from require "lapis.spec.db" 7 | import before_each from require "busted" 8 | 9 | with m = assert require("models")[model_name], "invalid model: #{model_name}" 10 | before_each -> 11 | truncate_tables m 12 | } 13 | -------------------------------------------------------------------------------- /spec/models/activity_logs_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.activity_logs", -> 4 | import Users from require "spec.models" 5 | import Categories, Topics, Posts, ActivityLogs from require "spec.community_models" 6 | 7 | it "should create activity log for post", -> 8 | post = factory.Posts! 9 | log = ActivityLogs\create { 10 | user_id: post.user_id 11 | object: post 12 | action: "create" 13 | data: {world: "cool"} 14 | } 15 | 16 | assert.same "create", log\action_name! 17 | 18 | it "should create activity log for topic", -> 19 | topic = factory.Topics! 20 | log = ActivityLogs\create { 21 | user_id: topic.user_id 22 | object: topic 23 | action: "delete" 24 | } 25 | 26 | assert.same "delete", log\action_name! 27 | 28 | 29 | -------------------------------------------------------------------------------- /spec/models/bookmarks_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.bookmarks", -> 4 | import Users from require "spec.models" 5 | import Topics, Bookmarks from require "spec.community_models" 6 | 7 | it "create a bookmark", -> 8 | user = factory.Users! 9 | topic = factory.Topics! 10 | 11 | assert Bookmarks\create { 12 | user_id: user.id 13 | object_type: "topic" 14 | object_id: topic.id 15 | } 16 | 17 | -------------------------------------------------------------------------------- /spec/models/category_groups_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.category_groups", -> 4 | import Users from require "spec.models" 5 | import Categories, CategoryGroups, 6 | CategoryGroupCategories from require "spec.community_models" 7 | 8 | it "should create category group", -> 9 | group = factory.CategoryGroups! 10 | group\refresh! 11 | assert.same 0, group.categories_count 12 | 13 | describe "with group", -> 14 | local group 15 | 16 | before_each -> 17 | group = factory.CategoryGroups! 18 | 19 | it "adds a category to a group", -> 20 | category = factory.Categories! 21 | group\add_category category 22 | 23 | assert.same 1, group.categories_count 24 | category\refresh! 25 | assert.same 1, category.category_groups_count 26 | 27 | gs = group\get_category_group_categories_paginated!\get_page! 28 | 29 | assert.same 1, #gs 30 | g = unpack gs 31 | 32 | assert.same category.id, g.category_id 33 | assert.same group.id, g.category_group_id 34 | 35 | group\add_category category 36 | group\refresh! 37 | assert.same 1, group.categories_count 38 | 39 | category\refresh! 40 | assert.same 1, category.category_groups_count 41 | 42 | c_group = category\get_category_group! 43 | assert.same group.id, c_group.id 44 | 45 | it "removes a category from group", -> 46 | category = factory.Categories! 47 | group\add_category category 48 | 49 | assert.same 1, group.categories_count 50 | group\remove_category category 51 | 52 | group\refresh! 53 | assert.same 0, group.categories_count 54 | 55 | category\refresh! 56 | assert.same 0, category.category_groups_count 57 | 58 | gs = group\get_category_group_categories_paginated!\get_page! 59 | assert.same {}, gs 60 | 61 | group\remove_category category 62 | 63 | it "sets categories", -> 64 | category1 = factory.Categories! 65 | category2 = factory.Categories! 66 | 67 | group\add_category category1 68 | 69 | group\set_categories { category2 } 70 | 71 | cats = {cgc.category_id, true for cgc in *CategoryGroupCategories\select!} 72 | 73 | assert.same { 74 | [category2.id]: true 75 | }, cats 76 | 77 | 78 | it "gets categories", -> 79 | category1 = factory.Categories! 80 | category2 = factory.Categories! 81 | category3 = factory.Categories! 82 | 83 | group\add_category category1 84 | group\add_category category2 85 | 86 | categories = group\get_categories_paginated!\get_all! 87 | category_ids = {c.id, true for c in *categories} 88 | assert.same { 89 | [category1.id]: true 90 | [category2.id]: true 91 | }, category_ids 92 | 93 | 94 | -------------------------------------------------------------------------------- /spec/models/category_tags_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.category_tags", -> 4 | import Users from require "spec.models" 5 | import Categories, Topics, Posts, CategoryTags from require "spec.community_models" 6 | 7 | it "creates tag for category", -> 8 | category = factory.Categories! 9 | tag = CategoryTags\create { 10 | slug: "hello-world" 11 | category_id: category.id 12 | } 13 | 14 | assert.truthy tag 15 | 16 | tags = category\get_tags! 17 | assert.same 1, #tags 18 | 19 | -------------------------------------------------------------------------------- /spec/models/moderation_logs_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.moderation_logs", -> 4 | import Users from require "spec.models" 5 | 6 | import 7 | Topics, ModerationLogs, Categories 8 | ModerationLogObjects, Posts 9 | from require "spec.community_models" 10 | 11 | describe "target action", -> 12 | it "gets action target", -> 13 | category = factory.Categories! 14 | 15 | log = factory.ModerationLogs { 16 | action: "topic.move" 17 | data: { 18 | target_category_id: category.id 19 | } 20 | } 21 | 22 | log\refresh! -- reload the json object 23 | assert.same "moved this topic to", log\get_action_text! 24 | target = assert log\get_action_target! 25 | assert.same category.id, target.id 26 | 27 | describe "create_backing_post", -> 28 | it "creates backing post", -> 29 | log = factory.ModerationLogs backing_post: false 30 | topic = log\get_object! 31 | category_order = topic.category_order 32 | 33 | log\create_backing_post! 34 | 35 | post = Posts\select! 36 | topic\refresh! 37 | 38 | -- doesn't count as reply, but works for pagination 39 | assert.same 0, topic.posts_count 40 | assert.same 1, topic.root_posts_count 41 | 42 | -- last post not updated 43 | assert.same nil, topic.last_post_id 44 | assert.same category_order, topic.category_order 45 | 46 | 47 | it "doesn't set last post to moderation log", -> 48 | log = factory.ModerationLogs backing_post: false 49 | topic = log\get_object! 50 | 51 | posts = for i=1,2 52 | with post = factory.Posts topic_id: topic.id 53 | topic\increment_from_post post 54 | 55 | backing_post = log\create_backing_post! 56 | topic\refresh! 57 | 58 | assert.same posts[2].id, topic.last_post_id 59 | topic\refresh_last_post! 60 | assert.same posts[2].id, topic.last_post_id 61 | 62 | assert.same 2, topic.posts_count 63 | assert.same 3, topic.root_posts_count 64 | 65 | Topics\recount id: topic.id 66 | 67 | topic\refresh! 68 | assert.same 2, topic.posts_count 69 | assert.same 3, topic.root_posts_count 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/models/moderators_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.moderators", -> 4 | import Users from require "spec.models" 5 | import Categories, Moderators, Posts, Topics from require "spec.community_models" 6 | 7 | local current_user, mod 8 | 9 | before_each -> 10 | current_user = factory.Users! 11 | mod = factory.Moderators user_id: current_user.id 12 | 13 | it "gets all moderators for category", -> 14 | category = mod\get_object! 15 | assert.same {current_user.id}, [m.user_id for m in *category\get_moderators!] 16 | 17 | it "gets moderator for category", -> 18 | category = mod\get_object! 19 | mod = category\find_moderator current_user 20 | assert.truthy mod 21 | 22 | assert.same category.id, mod.object_id 23 | assert.same Moderators.object_types.category, mod.object_type 24 | assert.same current_user.id, mod.user_id 25 | 26 | it "lets moderator edit post in category", -> 27 | topic = factory.Topics category_id: mod.object_id 28 | post = factory.Posts topic_id: topic.id 29 | 30 | assert.truthy post\allowed_to_edit current_user 31 | 32 | it "doesn't let moderator edit post other category", -> 33 | post = factory.Posts! 34 | assert.falsy post\allowed_to_edit current_user 35 | 36 | -------------------------------------------------------------------------------- /spec/models/pending_posts_spec.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | factory = require "spec.factory" 3 | 4 | import types from require "tableshape" 5 | 6 | describe "models.pending_posts", -> 7 | import Users from require "spec.models" 8 | import PendingPosts, Categories, Topics, Posts, CommunityUsers from require "spec.community_models" 9 | 10 | it "creates a pending post", -> 11 | factory.PendingPosts! 12 | 13 | it "hard deletes pending post", -> 14 | pp = factory.PendingPosts! 15 | pp\delete! 16 | 17 | it "promotes pending post", -> 18 | pending = factory.PendingPosts! 19 | post = pending\promote! 20 | 21 | assert.same 1, Posts\count! 22 | assert.same 0, PendingPosts\count! 23 | 24 | 25 | user = post\get_user! 26 | cu = CommunityUsers\for_user user 27 | 28 | assert_cu = types.assert types.partial { 29 | posts_count: 1 30 | topics_count: 0 31 | } 32 | 33 | assert_cu cu 34 | 35 | 36 | it "promotes pending post with topic and category being updated", -> 37 | category = factory.Categories! 38 | 39 | topic = factory.Topics category_id: category.id 40 | 41 | category\increment_from_topic topic 42 | 43 | other_topic = factory.Topics category_id: category.id 44 | category\increment_from_topic other_topic 45 | 46 | pending = factory.PendingPosts { 47 | created_at: "2006-1-5" 48 | topic_id: topic.id 49 | body: "Hello world!" 50 | body_format: "markdown" 51 | } 52 | 53 | post = pending\promote! 54 | 55 | assert.same 1, Posts\count! 56 | assert.same 0, PendingPosts\count! 57 | 58 | assert_promoted_post = types.assert types.partial { 59 | parent_post_id: types.nil 60 | topic_id: topic.id 61 | body: "Hello world!" 62 | body_format: Posts.body_formats.markdown 63 | created_at: "2006-01-05 00:00:00" 64 | } 65 | 66 | assert_promoted_post post 67 | 68 | topic\refresh! 69 | assert.same post.id, topic.last_post_id 70 | category\refresh! 71 | assert.same topic.id, category.last_topic_id 72 | 73 | cu = CommunityUsers\for_user pending\get_user! 74 | 75 | assert_cu = types.assert types.partial { 76 | posts_count: 1 77 | topics_count: 0 78 | } 79 | 80 | assert_cu cu 81 | 82 | it "promotes pending post with parent", -> 83 | post = factory.Posts! 84 | topic = post\get_topic! 85 | 86 | pending = factory.PendingPosts { 87 | parent_post_id: post.id 88 | topic_id: topic.id 89 | } 90 | 91 | promoted = pending\promote! 92 | 93 | assert.same 2, Posts\count! 94 | assert.same 0, PendingPosts\count! 95 | 96 | assert_promoted_post = types.assert types.partial { 97 | parent_post_id: post.id 98 | topic_id: topic.id 99 | } 100 | 101 | assert_promoted_post promoted 102 | 103 | it "promotes a pending topic", -> 104 | category = factory.Categories! 105 | pending = factory.PendingPosts { 106 | topic_id: db.NULL 107 | category_id: category.id 108 | title: "Hello world topic" 109 | } 110 | 111 | pending\promote! 112 | 113 | assert.same 1, Posts\count! 114 | assert.same 0, PendingPosts\count! 115 | 116 | post = unpack Posts\select! 117 | topic = post\get_topic! 118 | 119 | assert.same pending.title, topic.title 120 | assert.same pending.category_id, topic.category_id 121 | 122 | assert.same pending.body, post.body 123 | 124 | category\refresh! 125 | assert.same 1, category.topics_count 126 | assert.same topic.id, category.last_topic_id 127 | 128 | cu = CommunityUsers\for_user pending\get_user! 129 | 130 | assert types.partial({ 131 | topics_count: 1 132 | posts_count: 1 133 | }) cu 134 | 135 | 136 | -------------------------------------------------------------------------------- /spec/models/posts_search_spec.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | factory = require "spec.factory" 3 | 4 | describe "models.posts", -> 5 | import Users from require "spec.models" 6 | import Categories, Topics, Posts, PostsSearch from require "spec.community_models" 7 | 8 | local snapshot 9 | 10 | before_each -> 11 | snapshot = assert\snapshot! 12 | current_user = factory.Users! 13 | 14 | after_each -> 15 | snapshot\revert! 16 | 17 | it "indexes a post", -> 18 | post = factory.Posts body: "Hello how are you" 19 | topic = post\get_topic! 20 | topic\update title: "This Is My Topic" 21 | post.should_index_for_search = -> true 22 | 23 | -- insert initial 24 | search = assert post\refresh_search_index! 25 | assert.same post.id, search.post_id 26 | assert.same topic.id, search.topic_id 27 | assert.same topic\get_category!.id, search.category_id 28 | assert.same post.created_at, search.posted_at 29 | 30 | post\refresh! 31 | -- update it 32 | post.should_index_for_search = -> true 33 | post\update body: "another topic with another description" 34 | assert post\refresh_search_index! 35 | 36 | it "removes post that no longer needs to be indexed", -> 37 | post = factory.Posts! 38 | assert PostsSearch\index_post post 39 | 40 | post.should_index_for_search = -> false 41 | post\refresh_search_index! 42 | 43 | assert.same 0, PostsSearch\count! 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /spec/models/topic_participants_spec.moon: -------------------------------------------------------------------------------- 1 | factory = require "spec.factory" 2 | 3 | describe "models.topic_participants", -> 4 | import Users from require "spec.models" 5 | import TopicParticipants from require "spec.community_models" 6 | 7 | it "should participate a user", -> 8 | TopicParticipants\increment -1, 1 9 | ps = TopicParticipants\select! 10 | assert.same 1, #ps 11 | p = unpack ps 12 | 13 | assert.same -1, p.topic_id 14 | assert.same 1, p.user_id 15 | assert.same 1, p.posts_count 16 | 17 | it "should increment participant", -> 18 | TopicParticipants\increment -1, 1 19 | TopicParticipants\increment -1, 1 20 | 21 | ps = TopicParticipants\select! 22 | assert.same 1, #ps 23 | p = unpack ps 24 | 25 | assert.same -1, p.topic_id 26 | assert.same 1, p.user_id 27 | assert.same 2, p.posts_count 28 | 29 | it "should decrement participant", -> 30 | TopicParticipants\increment -1, 1 31 | TopicParticipants\increment -1, 1 32 | TopicParticipants\decrement -1, 1 33 | 34 | ps = TopicParticipants\select! 35 | assert.same 1, #ps 36 | p = unpack ps 37 | 38 | assert.same -1, p.topic_id 39 | assert.same 1, p.user_id 40 | assert.same 1, p.posts_count 41 | 42 | it "should decrement and delete participant", -> 43 | TopicParticipants\increment -1, 1 44 | TopicParticipants\decrement -1, 1 45 | 46 | ps = TopicParticipants\select! 47 | assert.same 0, #ps 48 | 49 | it "should un-participate an unincluded user", -> 50 | TopicParticipants\decrement -1, 1 51 | ps = TopicParticipants\select! 52 | assert.same 0, #ps 53 | -------------------------------------------------------------------------------- /spec/models/warnings.moon: -------------------------------------------------------------------------------- 1 | db = require "lapis.db" 2 | factory = require "spec.factory" 3 | 4 | import types from require "tableshape" 5 | 6 | date = require "date" 7 | 8 | describe "models.warnings", -> 9 | import Users from require "spec.models" 10 | import Warnings, CommunityUsers from require "spec.community_models" 11 | 12 | it "creates a warning", -> 13 | user = factory.Users! 14 | w = Warnings\create { 15 | user_id: user.id 16 | duration: "1 day" 17 | reason: "You did something bad" 18 | } 19 | 20 | cu = CommunityUsers\for_user user 21 | 22 | assert_warning = types.assert types.shape { 23 | types.partial { 24 | id: w.id 25 | user_id: cu.user_id 26 | } 27 | } 28 | 29 | assert_warning cu\get_active_warnings! 30 | 31 | it "makes a warning active", -> 32 | user = factory.Users! 33 | w = Warnings\create { 34 | user_id: user.id 35 | duration: "1 day" 36 | reason: "You did something bad" 37 | } 38 | 39 | -- it's active because it hasn't been started yet 40 | assert.same true, w\is_active!, "Unstarted warning should be active" 41 | 42 | assert.same true, (w\start_warning!), "Unstarted warning should start" 43 | 44 | assert.same true, w\is_active!, "Started warning should be active" 45 | 46 | expected_warning = types.partial { 47 | expires_at: types.string 48 | first_seen_at: types.string 49 | } 50 | 51 | assert expected_warning w 52 | 53 | expires_at = w.expires_at 54 | 55 | duration = date.diff(date(w.expires_at), date(w.first_seen_at))\spandays! 56 | 57 | -- we multiply by 10 and floor to compare floats safely 58 | assert.same 10, math.floor(duration*10), "Duration should be 1 day" 59 | 60 | -- starting an already started warning has no effect 61 | assert.same false, (w\start_warning!) 62 | 63 | assert.true w\end_warning!, "active warning should be expired immediately" 64 | assert.not.same w.expires_at, expires_at, "expires_at should be changed when immediately expiring" 65 | assert.false w\is_active!, "ended warning is no longer active" 66 | 67 | it "expired warning", -> 68 | user = factory.Users! 69 | w = Warnings\create { 70 | user_id: user.id 71 | duration: "1 day" 72 | reason: "You did something bad" 73 | first_seen_at: db.raw "date_trunc('second', now() at time zone 'utc') - '10 days'::interval" 74 | expires_at: db.raw "date_trunc('second', now() at time zone 'utc') - '9 days'::interval" 75 | } 76 | 77 | assert.false w\is_active!, "expired warning should not be active" 78 | assert.false w\end_warning!, "you can't end an expired warning" 79 | 80 | -- it should not return expired warning 81 | cu = CommunityUsers\for_user user 82 | assert.same {}, cu\get_active_warnings! 83 | 84 | -------------------------------------------------------------------------------- /views/Tupfile: -------------------------------------------------------------------------------- 1 | include_rules 2 | -------------------------------------------------------------------------------- /views/category.moon: -------------------------------------------------------------------------------- 1 | class Category extends require "widgets.base" 2 | inner_content: => 3 | h1 @category.title 4 | if @user 5 | p -> 6 | text "Created by " 7 | a href: @url_for("user", user_id: @user.id), @user\name_for_display! 8 | 9 | ul -> 10 | li -> 11 | a href: @url_for("new_topic", category_id: @category.id), "New topic" 12 | 13 | if @category\allowed_to_edit @current_user 14 | li -> 15 | a href: @url_for("edit_category", category_id: @category.id), "Edit category" 16 | 17 | if @category\allowed_to_moderate @current_user 18 | li -> 19 | a href: @url_for("category_moderators", category_id: @category.id), "Moderators" 20 | li -> 21 | a href: @url_for("category_members", category_id: @category.id), "Members" 22 | 23 | h3 "Topics" 24 | 25 | p -> 26 | strong "Count" 27 | text " " 28 | text @category.topics_count 29 | 30 | 31 | if @sticky_topics and next @sticky_topics 32 | @render_topics @sticky_topics 33 | 34 | @render_topics @topics 35 | 36 | render_topics: (topics) => 37 | element "table", border: "1", -> 38 | thead -> 39 | tr -> 40 | td "L" 41 | td "S" 42 | td "id" 43 | td "Title" 44 | td "Poster" 45 | td "Posts" 46 | td "Posted" 47 | td "Views" 48 | td "Last post" 49 | 50 | for topic in *topics 51 | tr -> 52 | td -> 53 | if topic.locked 54 | raw "✓" 55 | 56 | td -> 57 | if topic.sticky 58 | raw "✓" 59 | 60 | td topic.id 61 | 62 | td -> 63 | text "(#{topic.category_order}) " 64 | (topic\has_unread(@current_user) and strong or span) -> 65 | a href: @url_for("topic", topic_id: topic.id), topic.title 66 | 67 | td -> 68 | a href: @url_for("user", user_id: topic.user.id), topic.user\name_for_display! 69 | 70 | td tostring topic.posts_count 71 | 72 | td topic.created_at 73 | td topic.views_count 74 | 75 | td -> 76 | if seen = topic.user_topic_last_seen 77 | text "(seen #{seen.post_id}) " 78 | 79 | text topic.last_post_id 80 | text " " 81 | 82 | if last_post = topic.last_post 83 | text "by " 84 | a href: @url_for("user", user_id: last_post.user.id), last_post.user\name_for_display! 85 | text " on " 86 | text last_post.created_at 87 | 88 | p -> 89 | cat_opts = {category_id: @category.id } 90 | 91 | if @next_page 92 | a { 93 | href: @url_for "category", cat_opts, @next_page 94 | "Next page" 95 | } 96 | 97 | text " " 98 | 99 | if @prev_page 100 | a { 101 | href: @url_for "category", cat_opts, @prev_page 102 | "Previous page" 103 | } 104 | 105 | text " " 106 | 107 | a { 108 | href: @url_for "category", cat_opts 109 | "First page" 110 | } 111 | 112 | text " " 113 | 114 | -------------------------------------------------------------------------------- /views/category_accept_member.moon: -------------------------------------------------------------------------------- 1 | class CategoryAcceptMember extends require "widgets.base" 2 | inner_content: => 3 | h2 -> 4 | text "Join the community " 5 | 6 | a href: @url_for("category", category_id: @category.id), 7 | @category.title 8 | 9 | form method: "post", -> 10 | button "Accept" 11 | 12 | p "Don't want to accept? Just ignore this page" 13 | 14 | -------------------------------------------------------------------------------- /views/category_accept_moderator.moon: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CategoryAcceptModerator extends require "widgets.base" 4 | inner_content: => 5 | h2 -> 6 | text "Accept moderator position for " 7 | a href: @url_for("category", category_id: @category.id), 8 | @category.title 9 | 10 | form method: "post", -> 11 | button "Accept" 12 | 13 | p "Don't want to accept? Just ignore this page" 14 | 15 | -------------------------------------------------------------------------------- /views/category_members.moon: -------------------------------------------------------------------------------- 1 | class CategoryMembers extends require "widgets.base" 2 | 3 | inner_content: => 4 | h2 -> 5 | a href: @url_for("category", category_id: @category.id), @category.title 6 | text " members" 7 | 8 | ul -> 9 | if @category\allowed_to_edit_members @current_user 10 | li -> 11 | a href: @url_for("category_new_member", category_id: @category.id), "Add member" 12 | 13 | 14 | element "table", border: 1, -> 15 | thead -> 16 | tr -> 17 | td "Member" 18 | td "Accepted" 19 | td "Accept url" 20 | td "Remove" 21 | 22 | for member in *@members 23 | user = member\get_user! 24 | @dump member 25 | 26 | tr -> 27 | td -> 28 | a href: @url_for("user", user_id: user.id), user\name_for_display! 29 | 30 | td -> 31 | if member.accepted 32 | raw "✓" 33 | 34 | td -> 35 | return if member.accepted 36 | a href: @url_for("category_accept_member", category_id: @category.id), 37 | "Link" 38 | 39 | td -> 40 | form { 41 | action: @url_for "category_remove_member", category_id: @category.id, user_id: user.id 42 | method: "post" 43 | }, -> 44 | button "Remove" 45 | -------------------------------------------------------------------------------- /views/category_moderators.moon: -------------------------------------------------------------------------------- 1 | 2 | class CategoryModerators extends require "widgets.base" 3 | inner_content: => 4 | h2 -> 5 | a href: @url_for("category", category_id: @category.id), @category.title 6 | text " moderators" 7 | 8 | ul -> 9 | if @category\allowed_to_edit_moderators @current_user 10 | li -> 11 | a href: @url_for("category_new_moderator", category_id: @category.id), "New moderator" 12 | 13 | element "table", border: 1, -> 14 | thead -> 15 | tr -> 16 | td "Moderator" 17 | td "Accepted" 18 | td "Admin" 19 | td "Accept url" 20 | td "Remove" 21 | 22 | for mod in *@moderators 23 | user = mod\get_user! 24 | tr -> 25 | td -> 26 | a href: @url_for("user", user_id: user.id), user\name_for_display! 27 | 28 | td -> 29 | if mod.accepted 30 | raw "✓" 31 | 32 | td -> 33 | if mod.admin 34 | raw "✓" 35 | 36 | td -> 37 | return if mod.accepted 38 | a href: @url_for("category_accept_moderator", category_id: @category.id), 39 | "Link" 40 | 41 | td -> 42 | form { 43 | action: @url_for "category_remove_moderator", category_id: @category.id, user_id: user.id 44 | method: "post" 45 | }, -> 46 | button "Remove" 47 | 48 | 49 | unless next @moderators 50 | p -> 51 | em "There are no moderators" 52 | -------------------------------------------------------------------------------- /views/category_new_member.moon: -------------------------------------------------------------------------------- 1 | 2 | class CategoryNewMember extends require "widgets.base" 3 | inner_content: => 4 | p -> 5 | a href: @url_for("category_members", category_id: @category.id), 6 | "Return to members" 7 | 8 | h2 "New member" 9 | @render_errors! 10 | 11 | form method: "post", -> 12 | div -> 13 | label -> 14 | strong "Username" 15 | input type: "text", name: "username" 16 | 17 | button "Invite user" 18 | 19 | 20 | -------------------------------------------------------------------------------- /views/category_new_moderator.moon: -------------------------------------------------------------------------------- 1 | 2 | class CategoryNewModerator extends require "widgets.base" 3 | inner_content: => 4 | p -> 5 | a href: @url_for("category_moderators", category_id: @category.id), 6 | "Return to moderators" 7 | 8 | h2 "New moderator" 9 | @render_errors! 10 | 11 | form method: "post", -> 12 | div -> 13 | label -> 14 | strong "Username" 15 | input type: "text", name: "username" 16 | 17 | button "Invite user" 18 | 19 | 20 | -------------------------------------------------------------------------------- /views/delete_post.moon: -------------------------------------------------------------------------------- 1 | 2 | class DeletePost extends require "widgets.base" 3 | inner_content: => 4 | h2 "Delete post from #{@topic.title}" 5 | 6 | form method: "post", -> 7 | button "Delete" 8 | 9 | a href: @url_for("topic", topic_id: @topic.id), "Return to topic" 10 | 11 | 12 | -------------------------------------------------------------------------------- /views/edit_category.moon: -------------------------------------------------------------------------------- 1 | import Categories from require "community.models" 2 | 3 | class EditCategory extends require "widgets.base" 4 | inner_content: => 5 | h1 -> 6 | if @editing 7 | text "Editing category: #{@category.title}" 8 | else 9 | text "New category" 10 | 11 | @render_errors! 12 | 13 | form method: "post", -> 14 | div -> 15 | label -> 16 | strong "Title" 17 | input type: "text", name: "category[title]", value: @category and @category.title 18 | 19 | div -> 20 | label -> 21 | strong "Short description" 22 | input type: "text", name: "category[short_description]", value: @category and @category.short_description 23 | 24 | div -> 25 | label -> 26 | strong "Description" 27 | textarea name: "category[description]", @category and @category.description 28 | 29 | strong "Membership type" 30 | 31 | @radio_buttons "category[membership_type]", 32 | Categories.membership_types, 33 | @category and @category\get_membership_type! 34 | 35 | strong "Voting type" 36 | @radio_buttons "category[voting_type]", 37 | Categories.voting_types, 38 | @category and @category\get_voting_type! 39 | 40 | button -> 41 | if @editing 42 | text "Save" 43 | else 44 | text "New category" 45 | 46 | radio_buttons: (name, enum, val) => 47 | for key in *enum 48 | div -> 49 | label -> 50 | input { 51 | type: "radio" 52 | name: name 53 | value: key 54 | checked: enum[key] == val and "checked" or nil 55 | } 56 | 57 | text " #{key}" 58 | -------------------------------------------------------------------------------- /views/edit_post.moon: -------------------------------------------------------------------------------- 1 | class EditPost extends require "widgets.base" 2 | inner_content: => 3 | if @topic 4 | p -> 5 | a href: @url_for("topic", topic_id: @topic.id), "Return to topic" 6 | 7 | h1 -> 8 | if @editing 9 | text "Edit post" 10 | else 11 | if @parent_post 12 | text "Reply to post" 13 | else 14 | text "New post" 15 | 16 | @render_errors! 17 | 18 | form method: "post", -> 19 | if @parent_post 20 | input type: "hidden", name: "parent_post_id", value: @parent_post.id 21 | 22 | if @editing and @post\is_topic_post! and not @topic.permanent 23 | div -> 24 | label -> 25 | strong "Title" 26 | input type: "text", name: "post[title]", value: @topic and @topic.title 27 | 28 | div -> 29 | label -> 30 | strong "Body" 31 | textarea name: "post[body]", @post and @post.body or nil 32 | 33 | button -> 34 | if @editing 35 | text "Save" 36 | else 37 | text "Post" 38 | 39 | 40 | if @parent_post 41 | hr! 42 | h3 "Replying to" 43 | p @parent_post.body 44 | user = @parent_post\get_user! 45 | 46 | p -> 47 | em @parent_post.created_at 48 | 49 | p -> 50 | a href: @url_for("user", user_id: user.id), user\name_for_display! 51 | 52 | -------------------------------------------------------------------------------- /views/index.moon: -------------------------------------------------------------------------------- 1 | 2 | import Categories from require "community.models" 3 | 4 | class Index extends require "widgets.base" 5 | inner_content: => 6 | h1 "Index" 7 | 8 | if @current_user 9 | p -> 10 | text "You are logged in as " 11 | strong @current_user\name_for_display! 12 | 13 | ul -> 14 | if @current_user 15 | li -> 16 | a href: @url_for("new_category"), "Create category" 17 | else 18 | li -> 19 | a href: @url_for("register"), "Register" 20 | 21 | li -> 22 | a href: @url_for("login"), "Login" 23 | 24 | h2 "Categories" 25 | element "table", border: 1, -> 26 | thead -> 27 | tr -> 28 | td "Category" 29 | td "Type" 30 | td "Topics count" 31 | td "Creator" 32 | td "Last topic" 33 | 34 | for cat in *@categories 35 | tr -> 36 | td -> 37 | a href: @url_for("category", category_id: cat.id), cat.title 38 | 39 | td Categories.membership_types[cat.membership_type] 40 | td cat.topics_count 41 | td -> 42 | if user = cat\get_user! 43 | a href: @url_for("user", user_id: user.id), user\name_for_display! 44 | 45 | td -> 46 | if topic = cat\get_last_topic! 47 | a href: @url_for("topic", topic_id: topic.id), topic.title 48 | 49 | unless next @categories 50 | p -> em "There are no categories" 51 | 52 | -------------------------------------------------------------------------------- /views/layout.moon: -------------------------------------------------------------------------------- 1 | html = require "lapis.html" 2 | class Layout extends html.Widget 3 | content: => 4 | html_5 -> 5 | head -> title @title or "Community test" 6 | 7 | body -> 8 | div -> 9 | text "Logged in as: #{@current_user and @current_user.username}" 10 | 11 | 12 | text " - " 13 | a href: @url_for("index"), "Home" 14 | 15 | hr! 16 | 17 | @content_for "inner" 18 | 19 | -------------------------------------------------------------------------------- /views/lock_topic.moon: -------------------------------------------------------------------------------- 1 | class LockTopic extends require "widgets.base" 2 | inner_content: => 3 | p -> 4 | text "Lock topic " 5 | a href: @url_for("topic", topic_id: @topic.id), @topic.title 6 | text "?" 7 | 8 | form method: "post", -> 9 | label -> 10 | strong "Reason" 11 | textarea name: "reason", placeholder: "optional, will be shown on top of topic" 12 | 13 | br! 14 | 15 | button "Lock topic" 16 | -------------------------------------------------------------------------------- /views/login.moon: -------------------------------------------------------------------------------- 1 | class Login extends require "widgets.base" 2 | inner_content: => 3 | h1 "Login" 4 | @render_errors! 5 | form method: "post", -> 6 | label -> 7 | strong "Username" 8 | input type: "text", name: "username" 9 | 10 | button "Log in" 11 | 12 | 13 | h2 "Other" 14 | ul -> 15 | li -> 16 | a href: @url_for("register"), "Register" 17 | 18 | -------------------------------------------------------------------------------- /views/new_category.moon: -------------------------------------------------------------------------------- 1 | require "views.edit_category" 2 | -------------------------------------------------------------------------------- /views/new_post.moon: -------------------------------------------------------------------------------- 1 | require "views.edit_post" 2 | -------------------------------------------------------------------------------- /views/new_topic.moon: -------------------------------------------------------------------------------- 1 | class NewTopic extends require "widgets.base" 2 | inner_content: => 3 | h1 "New topic" 4 | @render_errors! 5 | 6 | form method: "post", -> 7 | div -> 8 | label -> 9 | strong "Title" 10 | input type: "text", name: "topic[title]" 11 | 12 | div -> 13 | label -> 14 | strong "Body" 15 | textarea name: "topic[body]" 16 | 17 | if @category\allowed_to_moderate @current_user 18 | div -> 19 | label -> 20 | input type: "checkbox", name: "topic[sticky]" 21 | text " Sticky" 22 | 23 | div -> 24 | label -> 25 | input type: "checkbox", name: "topic[locked]" 26 | text " Locked" 27 | 28 | button "New topic" 29 | -------------------------------------------------------------------------------- /views/post.moon: -------------------------------------------------------------------------------- 1 | Posts = require "widgets.posts" 2 | 3 | class Post extends require "widgets.base" 4 | inner_content: => 5 | widget Posts posts: { @post } 6 | -------------------------------------------------------------------------------- /views/register.moon: -------------------------------------------------------------------------------- 1 | class Register extends require "widgets.base" 2 | inner_content: => 3 | h1 "Register" 4 | @render_errors! 5 | form method: "post", -> 6 | label -> 7 | strong "Username" 8 | input type: "text", name: "username" 9 | 10 | button "New account" 11 | 12 | -------------------------------------------------------------------------------- /views/reply_post.moon: -------------------------------------------------------------------------------- 1 | require "views.edit_post" 2 | -------------------------------------------------------------------------------- /views/stick_topic.moon: -------------------------------------------------------------------------------- 1 | class StickTopic extends require "widgets.base" 2 | inner_content: => 3 | p -> 4 | text "Stick topic " 5 | a href: @url_for("topic", topic_id: @topic.id), @topic.title 6 | text "?" 7 | 8 | form method: "post", -> 9 | label -> 10 | strong "Reason" 11 | textarea name: "reason", placeholder: "optional, will be shown on top of topic" 12 | 13 | br! 14 | 15 | button "Stick topic" 16 | 17 | -------------------------------------------------------------------------------- /views/topic.moon: -------------------------------------------------------------------------------- 1 | Posts = require "widgets.posts" 2 | 3 | class Topic extends require "widgets.base" 4 | inner_content: => 5 | if @topic.category_id 6 | a href: @url_for("category", category_id: @topic\get_category!.id), @topic\get_category!.title 7 | 8 | h1 @topic.title 9 | 10 | p -> 11 | strong "Post count" 12 | text " " 13 | text @topic.posts_count 14 | 15 | if @topic.locked 16 | fieldset -> 17 | log = @topic\get_lock_log! 18 | p -> 19 | em "This topic is locked" 20 | 21 | @moderation_log_data log 22 | 23 | if @topic\allowed_to_moderate @current_user 24 | form action: @url_for("unlock_topic", topic_id: @topic.id), method: "post", -> 25 | button "Unlock" 26 | 27 | if @topic.sticky 28 | fieldset -> 29 | log = @topic\get_sticky_log! 30 | p -> 31 | em "This topic is sticky" 32 | 33 | @moderation_log_data log 34 | 35 | if @topic\allowed_to_moderate @current_user 36 | form action: @url_for("unstick_topic", topic_id: @topic.id), method: "post", -> 37 | button "Unstick" 38 | 39 | ul -> 40 | unless @topic.locked 41 | li -> 42 | a href: @url_for("new_post", topic_id: @topic.id), "Reply" 43 | 44 | if @topic\allowed_to_moderate @current_user 45 | unless @topic.locked 46 | li -> 47 | a href: @url_for("lock_topic", topic_id: @topic.id), "Lock" 48 | 49 | unless @topic.sticky 50 | li -> 51 | a href: @url_for("stick_topic", topic_id: @topic.id), "Stick" 52 | 53 | @pagination! 54 | hr! 55 | 56 | widget Posts! 57 | 58 | @pagination! 59 | 60 | pagination: => 61 | topic_opts = { topic_id: @topic.id } 62 | 63 | if @next_page 64 | a { 65 | href: @url_for "topic", topic_opts, @next_page 66 | "Next page" 67 | } 68 | 69 | text " " 70 | 71 | if @prev_page 72 | a { 73 | href: @url_for "topic", topic_opts, @prev_page 74 | "Previous page" 75 | } 76 | 77 | 78 | moderation_log_data: (log) => 79 | return unless log 80 | log_user = log\get_user! 81 | p -> 82 | em "By #{log_user\name_for_display!} on #{log.created_at}" 83 | if log.reason 84 | em ": #{log.reason}" 85 | 86 | -------------------------------------------------------------------------------- /views/user.moon: -------------------------------------------------------------------------------- 1 | 2 | class User extends require "widgets.base" 3 | inner_content: => 4 | h1 "#{@user\name_for_display!}" 5 | 6 | element "table", border: 1, -> 7 | for k in *{"topics_count", "posts_count", "votes_count"} 8 | tr -> 9 | td -> strong k 10 | td @community_user[k] 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /widgets/Tupfile: -------------------------------------------------------------------------------- 1 | include_rules 2 | -------------------------------------------------------------------------------- /widgets/base.moon: -------------------------------------------------------------------------------- 1 | import Widget from require "lapis.html" 2 | 3 | import underscore, time_ago_in_words from require "lapis.util" 4 | 5 | import random from math 6 | 7 | class Base extends Widget 8 | @widget_name: => underscore @__name or "some_widget" 9 | base_widget: true 10 | 11 | inner_content: => 12 | 13 | content: (fn=@inner_content) => 14 | classes = @widget_classes! 15 | 16 | local inner 17 | classes ..= " base_widget" if @base_widget 18 | 19 | @_opts = { class: classes, -> raw inner } 20 | 21 | if @js_init 22 | @widget_id! 23 | @content_for "js_init", -> raw @js_init! 24 | 25 | inner = capture -> fn @ 26 | element @elm_type or "div", @_opts 27 | 28 | widget_classes: => 29 | @css_class or @@widget_name! 30 | 31 | widget_id: => 32 | unless @_widget_id 33 | @_widget_id = "#{@@widget_name!}_#{random 0, 100000}" 34 | @_opts.id or= @_widget_id if @_opts 35 | @_widget_id 36 | 37 | widget_selector: => 38 | "'##{@widget_id!}'" 39 | 40 | 41 | render_errors: => 42 | return unless @errors and next @errors 43 | h3 "There was an errror" 44 | ul -> 45 | for e in *@errors 46 | li e 47 | 48 | dump: (thing) => 49 | pre require("moon").dump thing 50 | 51 | -------------------------------------------------------------------------------- /widgets/posts.moon: -------------------------------------------------------------------------------- 1 | 2 | class Posts extends require "widgets.base" 3 | @needs: { 4 | "posts" 5 | "topic" 6 | } 7 | 8 | inner_content: => 9 | for post in *@posts 10 | @render_post post 11 | 12 | render_post: (post) => 13 | vote_types = @topic\available_vote_types! 14 | 15 | if post.deleted 16 | div class: "post deleted", -> 17 | em "This post has been deleted" 18 | elseif post.block 19 | div class: "post deleted", -> 20 | em "You have blocked this user (#{post.user\name_for_display!})" 21 | form action: @url_for("unblock_user", blocked_user_id: post.user_id), method: "post", -> 22 | button "Unblock" 23 | 24 | else 25 | div class: "post", -> 26 | u -> 27 | a href: @url_for("post", post_id: post.id), "##{post.post_number}" 28 | 29 | text " " 30 | 31 | strong post.user\name_for_display! 32 | text " " 33 | em post.created_at 34 | em " (#{post.id})" 35 | 36 | if post.parent_post_id 37 | em " (parent: #{post.parent_post_id})" 38 | 39 | if post.edits_count > 0 40 | em " (#{post.edits_count} edits)" 41 | 42 | if vote_types.up 43 | em " (+#{post.up_votes_count})" 44 | 45 | if vote_types.down 46 | em " (-#{post.down_votes_count})" 47 | 48 | p post.body 49 | 50 | fieldset -> 51 | legend "Post tools" 52 | if post\allowed_to_edit @current_user 53 | p -> 54 | a href: @url_for("edit_post", post_id: post.id), "Edit" 55 | raw " · " 56 | a href: @url_for("delete_post", post_id: post.id), "Delete" 57 | 58 | if @current_user 59 | p -> 60 | a href: @url_for("reply_post", post_id: post.id), "Reply" 61 | 62 | form action: @url_for("block_user", blocked_user_id: post.user_id), method: "post", -> 63 | button "Block" 64 | 65 | form action: @url_for("vote_object", object_type: "post", object_id: post.id), method: "post", -> 66 | if vote_types.up 67 | button value: "up", name: "direction", "Upvote" 68 | 69 | if vote_types.up and vote_types.down 70 | raw " · " 71 | 72 | if vote_types.down 73 | button value: "down", name: "direction", "Downvote" 74 | 75 | if vote = post.vote 76 | if vote_types[vote\name!] 77 | text " You voted #{vote\name!}" 78 | raw " · " 79 | button value: "remove", name: "action", "Remove" 80 | 81 | if post.children and post.children[1] 82 | blockquote -> 83 | for child in *post.children 84 | @render_post child 85 | 86 | hr! 87 | 88 | --------------------------------------------------------------------------------