diff --git a/app.moon b/app.moon index 69b4202..fd47271 100644 --- a/app.moon +++ b/app.moon @@ -345,3 +345,6 @@ class MoonRocks extends lapis.Application @top_depended = [t for t in *@top_depended when t.manifest_module] render: true + + [timeline: "/timeline"]: require_login capture_errors_404 => + render: true diff --git a/applications/modules.moon b/applications/modules.moon index 7e2d9db..524e93f 100644 --- a/applications/modules.moon +++ b/applications/modules.moon @@ -210,6 +210,8 @@ class MoonRocksModules extends lapis.Application "invalid module" @flow("followings")\follow_object @module, @params.type + @flow("events")\create_event_and_deliver @module, @params.type + redirect_to: @url_for @module [unfollow_module: "/module/:module_id/unfollow/:type"]: require_login capture_errors_404 => @@ -222,4 +224,6 @@ class MoonRocksModules extends lapis.Application "invalid module" @flow("followings")\unfollow_object @module, @params.type + @flow("events")\remove_from_timeline @module, @params.type + redirect_to: @url_for @module diff --git a/applications/user.moon b/applications/user.moon index efeb62a..dffcd09 100644 --- a/applications/user.moon +++ b/applications/user.moon @@ -364,6 +364,7 @@ class MoonRocksUser extends lapis.Application "You can't follow yourself" @flow("followings")\follow_object followed_user, "subscription" + @flow("events")\create_event_and_deliver followed_user, "subscription" redirect_to: @url_for followed_user @@ -372,5 +373,6 @@ class MoonRocksUser extends lapis.Application "Invalid User" @flow("followings")\unfollow_object unfollowed_user, "subscription" + @flow("events")\remove_from_timeline unfollowed_user, "subscription" redirect_to: @url_for unfollowed_user diff --git a/flows/events.moon b/flows/events.moon new file mode 100644 index 0000000..5aa05ee --- /dev/null +++ b/flows/events.moon @@ -0,0 +1,60 @@ +import Flow from require "lapis.flow" + +import Events, Followings, TimelineEvents, Users from require "models" + +import assert_error from require "lapis.application" + +class EventsFlow extends Flow + expose_assigns: true + + new: (...) => + super ... + assert_error @current_user, "must be logged in" + + create_event_and_deliver: (object, event_type) => + import preload from require "lapis.db.model" + + -- Creates the primary event + event = Events\create({ + user: @current_user + object: object + event_type: Events.event_types\for_db event_type + }) + + -- Adds the new event to the timeline of every subscriber of @current_user + do + user_followers = Followings\select "where object_type = ? and object_id = ? and type = ?", Followings\object_type_for_object(@current_user), @current_user.id, Followings.types.subscription + preload user_followers, "source_user" + + for user in *user_followers + follower_user = user.source_user + + TimelineEvents\create({ + user_id: follower_user.id + event_id: event.id + }) + + -- If the event is a update, then every follower of the module should see the event + if Events.event_types.update == Events.event_types\for_db(event_type) + followers = Followings\select "where object_id = ? and object_type = ? and type = ?", event.object_id, event.object_type, Followings.types.subscription + + preload followers, "source_user" + + for users in *followers + follower_user = users.source_user + TimelineEvents\create({ + user_id: follower_user.id + event_id: event.id + }) + + remove_from_timeline: (object, event_type) => + db = require "lapis.db" + timeline_events = if Events\object_type_for_object(object) == Events\object_type_for_model(Users) + -- If we are removing the subscription from an use + db.select "user_id, event_id from timeline_events join events on timeline_events.event_id = events.id and events.source_user_id = ?", object.id + else + -- If we are removing the subscription from Module + db.select "user_id, event_id from timeline_events join events on timeline_events.event_id = events.id and events.source_user_id = ? and events.object_type = ? and object_id = ?", object.user_id, Events\object_type_for_object(object), object.id + + for timeline_entry in *timeline_events + TimelineEvents\delete(timeline_entry.user_id, timeline_entry.event_id) diff --git a/flows/followings.moon b/flows/followings.moon index 05a98e1..2f2d24e 100644 --- a/flows/followings.moon +++ b/flows/followings.moon @@ -1,7 +1,7 @@ import Flow from require "lapis.flow" -import Followings, Notifications from require "models" +import Events, Followings, Notifications, TimelineEvents, Users from require "models" import assert_error from require "lapis.application" @@ -13,6 +13,7 @@ class FollowingsFlow extends Flow assert_error @current_user, "must be logged in" follow_object: (object, type) => + f = Followings\create { source_user_id: @current_user.id :object diff --git a/migrations.moon b/migrations.moon index 8a8752d..a5678c2 100644 --- a/migrations.moon +++ b/migrations.moon @@ -278,4 +278,23 @@ import [1500318771]: => db.query "alter table followings drop constraint followings_pkey" db.query "alter table followings add primary key(source_user_id, object_type, object_id, type)" + + [1501182931]: => + create_table "events", { + {"id", serial} + {"event_type", enum} + {"source_user_id", foreign_key} + {"object_id", foreign_key} + {"object_type", enum} + {"created_at", time} + {"updated_at", time} + + "PRIMARY KEY (id)" + } + create_table "timeline_events", { + {"user_id", foreign_key} + {"event_id", foreign_key} + + "PRIMARY KEY (user_id, event_id)" + } } diff --git a/models/events.moon b/models/events.moon new file mode 100644 index 0000000..f895f15 --- /dev/null +++ b/models/events.moon @@ -0,0 +1,43 @@ +db = require "lapis.db" + +import Model, enum from require "lapis.db.model" +import safe_insert from require "helpers.models" + +class Events extends Model + @timestamp: true + + @event_types: enum { + subscription: 1 + bookmark: 2 + update: 3 + } + + @relations: { + {"source_user", belongs_to: "Users"} + {"object", polymorphic_belongs_to: { + [1]: {"module", "Modules"} + [2]: {"user", "Users"} + }} + } + + @create: (opts={}) => + assert opts.user, "missing event's user" + assert opts.object, "missing event's object" + assert opts.event_type, "missing event_type, events must have a type" + + event_opts = { + event_type: opts.event_type + source_user_id: opts.user.id + object_id: opts.object.id + object_type: @@object_type_for_object opts.object + } + + event = safe_insert @, event_opts + + return event + + delete: () => + import TimelineEvents from require "models" + + db.delete @@table_name!, { id: @id } + db.delete TimelineEvents\table_name!, { event_id: @id} diff --git a/models/timeline_events.moon b/models/timeline_events.moon new file mode 100644 index 0000000..d71e407 --- /dev/null +++ b/models/timeline_events.moon @@ -0,0 +1,30 @@ +db = require "lapis.db" +import Model from require "lapis.db.model" + + +class TimelineEvents extends Model + @primary_key: { "user_id", "event_id" } + + @relations: { + {"user", belongs_to: "Users"} + {"event", belongs_to: "Events"} + } + + @create: (opts={}) => + assert opts.user_id, "user id not specified" + assert opts.event_id, "event id not specified" + + super { + user_id: opts.user_id + event_id: opts.event_id + } + + @delete: (user_id, event_id) => + db.delete @table_name!, { user_id: user_id, event_id: event_id } + + @user_timeline: (user) => + import preload from require "lapis.db.model" + timeline = @@select "where user_id = ? limit 50", user.id + preload timeline, "user" + preload timeline, event: "object" + return timeline diff --git a/models/users.moon b/models/users.moon index 92a1a4f..1b31501 100644 --- a/models/users.moon +++ b/models/users.moon @@ -234,3 +234,7 @@ class Users extends Model uuid = generate_uuid() "#{username}-#{uuid\gsub("-", "")\sub 1, 10}" + + timeline: () => + import TimelineEvents from require "models" + TimelineEvents\user_timeline @ diff --git a/spec/applications/modules_spec.moon b/spec/applications/modules_spec.moon index 609db68..4c5c926 100644 --- a/spec/applications/modules_spec.moon +++ b/spec/applications/modules_spec.moon @@ -6,13 +6,13 @@ import request_as from require "spec.helpers" factory = require "spec.factory" -import Modules, Versions, Followings, Users, Notifications, NotificationObjects from require "models" + describe "applications.modules", -> + import Modules, Versions, Events, Followings, Users, Notifications, NotificationObjects from require "spec.models" use_test_server! before_each -> - truncate_tables Modules, Versions, Followings, Users, Notifications, NotificationObjects it "follows module", -> current_user = factory.Users! @@ -21,8 +21,12 @@ describe "applications.modules", -> assert.same 302, status followings = Followings\select! + events = Events\select! + user_timeline = current_user\timeline! assert.same 1, #followings + assert.same 1, #events + following = unpack followings assert.same current_user.id, following.source_user_id @@ -44,7 +48,10 @@ describe "applications.modules", -> assert.same 302, status followings = Followings\select! + events = Events\select! + assert.same 0, #followings + assert.same 0, #events current_user\refresh! mod\refresh! @@ -114,4 +121,3 @@ describe "applications.modules", -> assert.same 0, Notifications\count! assert.same 0, NotificationObjects\count! - diff --git a/spec/models/events_spec.moon b/spec/models/events_spec.moon new file mode 100644 index 0000000..c46b49b --- /dev/null +++ b/spec/models/events_spec.moon @@ -0,0 +1,79 @@ +import use_test_env from require "lapis.spec" + +factory = require "spec.factory" + +describe "models.events", -> + import Events, Modules, Users, TimelineEvents from require "spec.models" + + use_test_env! + + it "creates an event of user following user", -> + user = factory.Users! + followed_user = factory.Users! + + event = Events\create({ + user: user + object: followed_user + event_type: Events.event_types.subscription + }) + + user_timeline = user\timeline! + + assert.same user.id, event.source_user_id + assert.same followed_user.id, event.object_id + assert.same event.event_type, Events.event_types.subscription + + assert.same, #user_timeline, 1 + + it "creates an event of user following a module", -> + user = factory.Users! + module = factory.Modules! + + event = Events\create({ + user: user + object: module + event_type: Events.event_types.subscription + }) + + user_timeline = user\timeline! + + assert.same user.id, event.source_user_id + assert.same module.id, event.object_id + assert.same event.event_type, Events.event_types.subscription + + assert.same, #user_timeline, 1 + + it "creates an event of user starring a module", -> + user = factory.Users! + module = factory.Modules! + + event = Events\create({ + user: user + object: module + event_type: Events.event_types.bookmark + }) + + user_timeline = user\timeline! + + assert.same user.id, event.source_user_id + assert.same module.id, event.object_id + assert.same event.event_type, Events.event_types.bookmark + + assert.same, #user_timeline, 1 + + it "deletes an event", -> + user = factory.Users! + module = factory.Modules! + + event = Events\create({ + user: user + object: module + event_type: Events.event_types.bookmark + }) + + event_id = event.id + + event\delete! + + assert.same Events\find(event_id), nil + assert.same 0, #user\timeline! diff --git a/views/index.moon b/views/index.moon index 9fb067e..15cc606 100644 --- a/views/index.moon +++ b/views/index.moon @@ -14,7 +14,6 @@ class Index extends require "widgets.page" @inner_content! inner_content: => - div class: "home_columns", -> div class: "column", -> h2 -> @@ -63,4 +62,3 @@ class Index extends require "widgets.page" script type: "text/javascript", -> raw "new M.Index(#{@widget_selector!}, #{to_json @downloads_daily});" - diff --git a/views/timeline.moon b/views/timeline.moon new file mode 100644 index 0000000..e81b9ef --- /dev/null +++ b/views/timeline.moon @@ -0,0 +1,8 @@ +TimelineEvents = require "widgets.timeline_events" + +class Timeline extends require "widgets.page" + inner_content: => + h2 -> + text "Timeline" + widget TimelineEvents! + diff --git a/widgets/timeline_events.moon b/widgets/timeline_events.moon new file mode 100644 index 0000000..d81e3c0 --- /dev/null +++ b/widgets/timeline_events.moon @@ -0,0 +1,48 @@ +import Events, Modules, Users from require "models" +import time_ago_in_words from require "lapis.util" + +class TimelineEvents extends require "widgets.base" + @needs: { + "modules" + } + + inner_content: => + ul -> + timeline = @current_user\timeline! + + for timeline_event in *timeline + row_event = timeline_event.event + user = timeline_event.user + + message = switch row_event.event_type + when Events.event_types.subscription + " followed " + when Events.event_type.bookmark + " starred " + when Events.event_type.update + " delivered a new version of " + else + "" + li -> + span class: "author", -> + a href: @url_for("user_profile", user: user.slug), user\name_for_display! + text message + + switch Events\model_for_object_type(row_event.object_type) + when Modules + module = row_event.object + a { + class: "title", + href: @url_for("module", user: Users\find(module.user_id).slug, module: module.name) + }, module\name_for_display! + + text " module" + when Users + usr = row_event.object + a { + class: "title", + href: @url_for("user_profile", user: usr.slug) + }, usr\name_for_display! + else + "" + text " ", time_ago_in_words(row_event.created_at)