Skip to content

Commit 24ee72f

Browse files
Add document and library views (#239)
* basic view count * show view count * simplify * x --------- Co-authored-by: Vijay Swamidass <[email protected]>
1 parent bb1b634 commit 24ee72f

15 files changed

+1152
-17
lines changed

app/controllers/application_controller.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,18 @@ def user_not_authorized
9191
flash[:alert] = 'You are not authorized to perform this action.'
9292
redirect_back(fallback_location: root_path)
9393
end
94+
95+
# Tracks a view for any viewable object (Document, Library, etc.)
96+
# Updates the timestamp if the user has already viewed this item
97+
# @param viewable [ActiveRecord::Base] the object being viewed (must have viewed_items association)
98+
def track_view(viewable)
99+
return unless current_user
100+
101+
viewed_item = ViewedItem.find_or_initialize_by(
102+
user: current_user,
103+
viewable: viewable
104+
)
105+
viewed_item.viewed_at = Time.current
106+
viewed_item.save
107+
end
94108
end

app/controllers/dashboard_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@ def index
1111
.group('assistants.id')
1212
.order('last_chat_time DESC')
1313
.limit(5)
14+
@recently_viewed_documents = current_user.recently_viewed_documents(limit: 5)
15+
.includes(:viewed_items)
16+
@recently_viewed_libraries = current_user.recently_viewed_libraries(limit: 5)
17+
.includes(:viewed_items)
1418
end
1519
end

app/controllers/documents_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def edit
1515

1616
# GET /documents/1 or /documents/1.json
1717
def show
18+
# Track view for authenticated users
19+
track_view(@document)
20+
1821
# Handle flagging functionality (requires user to be logged in)
1922
if params[:flag] && current_user
2023
@document.disliked_by current_user, vote_scope: 'flag'

app/controllers/libraries_controller.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ def edit
1212
end
1313

1414
# GET /libraries/1 or /libraries/1.json
15-
def show; end
15+
def show
16+
# Track view for authenticated users
17+
track_view(@library)
18+
end
1619

1720
def users
1821
@library = Library.find(params[:id])

app/models/document.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ class Document < ApplicationRecord
99
has_many :comments, dependent: :destroy
1010
has_neighbors :embedding
1111

12+
# Recently viewed items feature
13+
has_many :viewed_items, as: :viewable, dependent: :destroy
14+
15+
# Returns the number of unique users who have viewed this document
16+
# Note: Due to the unique index on [user_id, viewable_type, viewable_id],
17+
# each user can only have one view record per document. When a user views
18+
# the document multiple times, only the timestamp is updated.
19+
# @return [Integer] the number of unique users who have viewed this document
20+
def unique_viewers
21+
viewed_items.count
22+
end
23+
24+
# Alias for backward compatibility
25+
alias total_views unique_viewers
26+
1227
# Primary search scope using PostgreSQL full-text search
1328
# Searches across both title and document content with strict word matching
1429
# - prefix: true allows partial word matching (e.g., "test" matches "testing")

app/models/library.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class Library < ApplicationRecord
1010
has_many :library_users, dependent: :destroy
1111
has_many :users, through: :library_users
1212

13+
# Recently viewed items feature
14+
has_many :viewed_items, as: :viewable, dependent: :destroy
15+
1316
pg_search_scope :search_by_name,
1417
against: %i[name description],
1518
using: {

app/models/user.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,44 @@ class User < ApplicationRecord
1717
has_many :assistants, through: :assistant_users
1818
has_many :comments, dependent: :destroy
1919

20+
# Recently viewed items feature
21+
has_many :viewed_items, dependent: :destroy
22+
23+
# Returns the most recently viewed documents for this user
24+
# @param limit [Integer] the maximum number of documents to return (default: 5)
25+
# @return [ActiveRecord::Relation<Document>] the recently viewed documents, ordered by most recent first
26+
def recently_viewed_documents(limit: 5)
27+
Document.joins(:viewed_items)
28+
.where(viewed_items: { user_id: id })
29+
.order('viewed_items.viewed_at DESC')
30+
.distinct
31+
.limit(limit)
32+
end
33+
34+
# Returns the most recently viewed libraries for this user
35+
# @param limit [Integer] the maximum number of libraries to return (default: 5)
36+
# @return [ActiveRecord::Relation<Library>] the recently viewed libraries, ordered by most recent first
37+
def recently_viewed_libraries(limit: 5)
38+
Library.joins(:viewed_items)
39+
.where(viewed_items: { user_id: id })
40+
.order('viewed_items.viewed_at DESC')
41+
.distinct
42+
.limit(limit)
43+
end
44+
45+
# Generic method to get recently viewed items of any type
46+
# @param viewable_type [String] the type of viewable items to retrieve (e.g., 'Document')
47+
# @param limit [Integer] the maximum number of items to return (default: 5)
48+
# @return [ActiveRecord::Relation] the recently viewed items
49+
def recently_viewed(viewable_type:, limit: 5)
50+
viewable_type.constantize
51+
.joins(:viewed_items)
52+
.where(viewed_items: { user_id: id })
53+
.order('viewed_items.viewed_at DESC')
54+
.distinct
55+
.limit(limit)
56+
end
57+
2058
private
2159

2260
def password_strength

app/models/viewed_item.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class ViewedItem < ApplicationRecord
4+
belongs_to :user
5+
belongs_to :viewable, polymorphic: true
6+
7+
validates :user_id, presence: true
8+
validates :viewable_id, presence: true
9+
validates :viewable_type, presence: true
10+
validates :viewed_at, presence: true
11+
12+
# Ensure uniqueness at the model level as well
13+
validates :user_id, uniqueness: { scope: %i[viewable_type viewable_id] }
14+
15+
# Scope to get most recently viewed items
16+
scope :recent, -> { order(viewed_at: :desc) }
17+
18+
# Scope to get items for a specific viewable type
19+
scope :for_type, ->(type) { where(viewable_type: type) }
20+
end

app/views/dashboard/index.html.erb

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
<h1 class="text-2xl font-bold text-gray-800 mb-6 flex items-center">
1212
My Activity
1313
</h1>
14-
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
14+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
1515
<!-- Questions -->
1616
<div class="bg-white rounded-lg p-6 border border-gray-300">
1717
<div class="flex justify-between items-center mb-4">
18-
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
18+
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
1919
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2020
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
2121
</svg>
@@ -44,7 +44,7 @@
4444
<!-- Chats -->
4545
<div class="bg-white rounded-lg p-6 border border-gray-300">
4646
<div class="flex justify-between items-center mb-4">
47-
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
47+
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
4848
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
4949
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
5050
</svg>
@@ -70,32 +70,65 @@
7070
<% end %>
7171
</div>
7272
</div>
73-
<!-- Assistants -->
73+
74+
<!-- Recently Viewed Documents -->
7475
<div class="bg-white rounded-lg p-6 border border-gray-300">
7576
<div class="flex justify-between items-center mb-4">
76-
<h2 class="text-lg font-semibold text-gray-700 flex items-center">
77+
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
7778
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
78-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
79+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
80+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
7981
</svg>
80-
Assistants
82+
Documents
8183
</h2>
82-
<%= link_to assistants_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
84+
<%= link_to documents_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
8385
More
8486
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8587
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
8688
</svg>
8789
<% end %>
8890
</div>
8991
<div class="">
90-
<% if @recent_assistants.any? %>
91-
<% @recent_assistants.each do |assistant| %>
92+
<% if @recently_viewed_documents.any? %>
93+
<% @recently_viewed_documents.each do |document| %>
94+
<% viewed_item = document.viewed_items.find_by(user: current_user) %>
9295
<%= render "dashboard/list_item",
93-
path: assistant_path(assistant),
94-
title: assistant.name,
95-
subtitle: "Last active #{time_ago_in_words(assistant.last_chat_time)} ago" %>
96+
path: document_path(document),
97+
title: document.title.truncate(50),
98+
subtitle: "Viewed #{time_ago_in_words(viewed_item.viewed_at)} ago" %>
9699
<% end %>
97100
<% else %>
98-
<p class="text-gray-400 italic">No recent assistants</p>
101+
<p class="text-gray-400 italic">No recently viewed documents</p>
102+
<% end %>
103+
</div>
104+
</div>
105+
<!-- Recently Viewed Libraries -->
106+
<div class="bg-white rounded-lg p-6 border border-gray-300">
107+
<div class="flex justify-between items-center mb-4">
108+
<h2 class="text-lg font-semibold text-gray-500 flex items-center">
109+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
110+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
111+
</svg>
112+
Libraries
113+
</h2>
114+
<%= link_to libraries_path, class: "inline-flex items-center px-3 py-1 border border-sky-500 text-sky-500 text-sm font-medium rounded-md hover:bg-sky-500 hover:text-white transition-colors duration-200" do %>
115+
More
116+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
117+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
118+
</svg>
119+
<% end %>
120+
</div>
121+
<div class="">
122+
<% if @recently_viewed_libraries.any? %>
123+
<% @recently_viewed_libraries.each do |library| %>
124+
<% viewed_item = library.viewed_items.find_by(user: current_user) %>
125+
<%= render "dashboard/list_item",
126+
path: library_path(library),
127+
title: library.name.truncate(50),
128+
subtitle: "Viewed #{time_ago_in_words(viewed_item.viewed_at)} ago" %>
129+
<% end %>
130+
<% else %>
131+
<p class="text-gray-400 italic">No recently viewed libraries</p>
99132
<% end %>
100133
</div>
101134
</div>

app/views/documents/_document.html.erb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"turbo-method": "delete",
2727
"turbo-confirm": "Are you sure you want to delete this document?"
2828
},
29-
class: "border text-red-700 border-red-300 py-3 px-5 bg-white text-sm #{'rounded-l-lg' if document.deleted?} rounded-r-lg hover:bg-red-50 hover:text-red-800" %>
29+
class: "border border-sky-500 text-red-700 py-3 px-5 bg-white text-sm #{'rounded-l-lg' if document.deleted?} rounded-r-lg hover:bg-red-50 hover:text-red-800" %>
3030
<% end %>
3131
<% end %>
3232
</div>
@@ -62,6 +62,15 @@
6262

6363
<!-- Voting and Flag Buttons on the right side -->
6464
<div class="flex items-center mx-4 space-x-2">
65+
<!-- Unique Viewers Count -->
66+
<div class="flex items-center px-3 py-1 text-sm bg-blue-50 text-blue-800 border border-blue-200 rounded-full" title="Unique viewers">
67+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 mr-1">
68+
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
69+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
70+
</svg>
71+
<%= document.unique_viewers %>
72+
</div>
73+
6574
<!-- Upvote/Downvote Buttons -->
6675
<% if current_user %>
6776
<% upvote_count = @document.votes_for.where(vote_scope: 'rating', vote_flag: true).count %>

0 commit comments

Comments
 (0)