├── Gemfile ├── Gemfile.lock ├── dispatcher.rb ├── documents.rb ├── forum.rb ├── friend.rb ├── joiner.rb ├── mix.rb ├── readme.md ├── reports.rb ├── rss.rb └── scratch.rb /Gemfile: -------------------------------------------------------------------------------- 1 | gem "rails" 2 | gem "sqlite3" 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | specs: 3 | actioncable (7.1.2) 4 | actionpack (= 7.1.2) 5 | activesupport (= 7.1.2) 6 | nio4r (~> 2.0) 7 | websocket-driver (>= 0.6.1) 8 | zeitwerk (~> 2.6) 9 | actionmailbox (7.1.2) 10 | actionpack (= 7.1.2) 11 | activejob (= 7.1.2) 12 | activerecord (= 7.1.2) 13 | activestorage (= 7.1.2) 14 | activesupport (= 7.1.2) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.1.2) 20 | actionpack (= 7.1.2) 21 | actionview (= 7.1.2) 22 | activejob (= 7.1.2) 23 | activesupport (= 7.1.2) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.2) 29 | actionpack (7.1.2) 30 | actionview (= 7.1.2) 31 | activesupport (= 7.1.2) 32 | nokogiri (>= 1.8.5) 33 | racc 34 | rack (>= 2.2.4) 35 | rack-session (>= 1.0.1) 36 | rack-test (>= 0.6.3) 37 | rails-dom-testing (~> 2.2) 38 | rails-html-sanitizer (~> 1.6) 39 | actiontext (7.1.2) 40 | actionpack (= 7.1.2) 41 | activerecord (= 7.1.2) 42 | activestorage (= 7.1.2) 43 | activesupport (= 7.1.2) 44 | globalid (>= 0.6.0) 45 | nokogiri (>= 1.8.5) 46 | actionview (7.1.2) 47 | activesupport (= 7.1.2) 48 | builder (~> 3.1) 49 | erubi (~> 1.11) 50 | rails-dom-testing (~> 2.2) 51 | rails-html-sanitizer (~> 1.6) 52 | activejob (7.1.2) 53 | activesupport (= 7.1.2) 54 | globalid (>= 0.3.6) 55 | activemodel (7.1.2) 56 | activesupport (= 7.1.2) 57 | activerecord (7.1.2) 58 | activemodel (= 7.1.2) 59 | activesupport (= 7.1.2) 60 | timeout (>= 0.4.0) 61 | activestorage (7.1.2) 62 | actionpack (= 7.1.2) 63 | activejob (= 7.1.2) 64 | activerecord (= 7.1.2) 65 | activesupport (= 7.1.2) 66 | marcel (~> 1.0) 67 | activesupport (7.1.2) 68 | base64 69 | bigdecimal 70 | concurrent-ruby (~> 1.0, >= 1.0.2) 71 | connection_pool (>= 2.2.5) 72 | drb 73 | i18n (>= 1.6, < 2) 74 | minitest (>= 5.1) 75 | mutex_m 76 | tzinfo (~> 2.0) 77 | base64 (0.2.0) 78 | bigdecimal (3.1.5) 79 | builder (3.2.4) 80 | concurrent-ruby (1.2.2) 81 | connection_pool (2.4.1) 82 | crass (1.0.6) 83 | date (3.3.4) 84 | drb (2.2.0) 85 | ruby2_keywords 86 | erubi (1.12.0) 87 | globalid (1.2.1) 88 | activesupport (>= 6.1) 89 | i18n (1.14.1) 90 | concurrent-ruby (~> 1.0) 91 | io-console (0.7.1) 92 | irb (1.11.1) 93 | rdoc 94 | reline (>= 0.4.2) 95 | loofah (2.22.0) 96 | crass (~> 1.0.2) 97 | nokogiri (>= 1.12.0) 98 | mail (2.8.1) 99 | mini_mime (>= 0.1.1) 100 | net-imap 101 | net-pop 102 | net-smtp 103 | marcel (1.0.2) 104 | mini_mime (1.1.5) 105 | minitest (5.21.1) 106 | mutex_m (0.2.0) 107 | net-imap (0.4.9.1) 108 | date 109 | net-protocol 110 | net-pop (0.1.2) 111 | net-protocol 112 | net-protocol (0.2.2) 113 | timeout 114 | net-smtp (0.4.0.1) 115 | net-protocol 116 | nio4r (2.7.0) 117 | nokogiri (1.16.0-arm64-darwin) 118 | racc (~> 1.4) 119 | psych (5.1.2) 120 | stringio 121 | racc (1.7.3) 122 | rack (3.0.8) 123 | rack-session (2.0.0) 124 | rack (>= 3.0.0) 125 | rack-test (2.1.0) 126 | rack (>= 1.3) 127 | rackup (2.1.0) 128 | rack (>= 3) 129 | webrick (~> 1.8) 130 | rails (7.1.2) 131 | actioncable (= 7.1.2) 132 | actionmailbox (= 7.1.2) 133 | actionmailer (= 7.1.2) 134 | actionpack (= 7.1.2) 135 | actiontext (= 7.1.2) 136 | actionview (= 7.1.2) 137 | activejob (= 7.1.2) 138 | activemodel (= 7.1.2) 139 | activerecord (= 7.1.2) 140 | activestorage (= 7.1.2) 141 | activesupport (= 7.1.2) 142 | bundler (>= 1.15.0) 143 | railties (= 7.1.2) 144 | rails-dom-testing (2.2.0) 145 | activesupport (>= 5.0.0) 146 | minitest 147 | nokogiri (>= 1.6) 148 | rails-html-sanitizer (1.6.0) 149 | loofah (~> 2.21) 150 | nokogiri (~> 1.14) 151 | railties (7.1.2) 152 | actionpack (= 7.1.2) 153 | activesupport (= 7.1.2) 154 | irb 155 | rackup (>= 1.0.0) 156 | rake (>= 12.2) 157 | thor (~> 1.0, >= 1.2.2) 158 | zeitwerk (~> 2.6) 159 | rake (13.1.0) 160 | rdoc (6.6.2) 161 | psych (>= 4.0.0) 162 | reline (0.4.2) 163 | io-console (~> 0.5) 164 | ruby2_keywords (0.0.5) 165 | sqlite3 (1.7.0-arm64-darwin) 166 | stringio (3.1.0) 167 | thor (1.3.0) 168 | timeout (0.4.1) 169 | tzinfo (2.0.6) 170 | concurrent-ruby (~> 1.0) 171 | webrick (1.8.1) 172 | websocket-driver (0.7.6) 173 | websocket-extensions (>= 0.1.0) 174 | websocket-extensions (0.1.5) 175 | zeitwerk (2.6.12) 176 | 177 | PLATFORMS 178 | arm64-darwin 179 | 180 | DEPENDENCIES 181 | rails 182 | sqlite3 183 | 184 | BUNDLED WITH 185 | 2.5.4 186 | -------------------------------------------------------------------------------- /dispatcher.rb: -------------------------------------------------------------------------------- 1 | # Dispatcher 2 | # Building a Dispatcher app for service companies to plan & schedule their onsites. 3 | 4 | class Location < AppliationRecord 5 | def address 6 | end 7 | end 8 | 9 | class Organization < AppliationRecord 10 | has_many :organization_jobs 11 | has_many :user_memberships 12 | has_many :users, through: :user_memberships 13 | end 14 | 15 | class Organization::Membership < AppliationRecord 16 | belongs_to :organization 17 | belongs_to :organization_user 18 | end 19 | 20 | class Organization::User < AppliationRecord 21 | has_many :organization_memberships 22 | end 23 | 24 | class Organization::Customer < AppliationRecord 25 | belongs_to :customer 26 | end 27 | 28 | class Organization::Job < AppliationRecord 29 | belongs_to :customer 30 | belongs_to :organization 31 | 32 | has_many :tasks 33 | has_many :onsites 34 | 35 | attribute :name 36 | end 37 | 38 | class Organization::Task < ApplicationRecord 39 | belongs_to :onsite 40 | 41 | attribute :description 42 | attribute :estimated_duration 43 | end 44 | 45 | class Organization::TaskAssignment < ApplicationRecord 46 | belongs_to :task 47 | belongs_to :onsite 48 | end 49 | 50 | class Organization::Onsite < ApplicationRecord 51 | belongs_to :job 52 | belongs_to :location 53 | 54 | has_one :calendar_events 55 | 56 | has_many :onsite_assignments 57 | has_many :onsite_tasks, through: :task_assignments 58 | 59 | enum status: [:draft, :finalized] 60 | 61 | attribute :label 62 | attribute :estimated_total_duration 63 | end 64 | 65 | class Organization::OnsiteAssignment < ApplicationRecord 66 | belongs_to :user 67 | belongs_to :onsite 68 | end 69 | 70 | class Calendar::Event 71 | belongs_to :preceding_event #?? 72 | belongs_to :organization_onsite 73 | 74 | attribute :started_at 75 | attribute :ended_at 76 | end 77 | 78 | class Calendar::EventBlob 79 | belongs_to :calendar_event 80 | 81 | attribute :url # https://dispatcher.com/onsite/2 82 | attribute :started_at 83 | attribute :ended_at 84 | attribute :address # location 85 | attribute :title # onsite 86 | attribute :attendees # onsite_assignments 87 | end 88 | -------------------------------------------------------------------------------- /documents.rb: -------------------------------------------------------------------------------- 1 | # A tool to help contract lawyers by organizing a cluttered collection of disorganized contracts and their versions in Google Drive (different/inconsistent naming, unreliable timestamps). 2 | 3 | # The tool aims to classify contracts, order their versions chronologically, and summarize changes between versions. 4 | 5 | # It updates the document index upon each new upload, whether the folder has been previously processed or not. 6 | 7 | 8 | # # Full Spec 9 | 10 | # I’m part of a team helping contract lawyers be more efficient at work. One of the core features is AI-assisted document version management. 11 | 12 | # Context 13 | # When a lawyer is negotiating a deal, there are a few different contracts involved, and each contract has many versions that go back and forth by email, and lawyers usually manually save the attachments into a shared folder. But this folder is a mess. The attachments have different names, the timestamps are unreliable, so it’s not useful when someone looks back at the file later on (ie, if there is a dispute regarding the contracts). They can’t make sense of the history of negotiations. 14 | 15 | # Business goal 16 | # So we are building a tool to help organize this messy dump of documents (PDF+Word), with unreliable timestamps and file names, and we will use LLMs to take a best guess at: 17 | 18 | # (a) what are the different types of contracts that are part of the deal, 19 | # (b) identify the different versions of each contract and the order of the versions (in time), and 20 | # (c) provide an incremental summary of the change between each version of the same contract. 21 | # Existing data 22 | 23 | # The existing database has a table of clients, and each client has many deals. 24 | # Each deal also has a link to a google drive folder URL, and this folder is a big messy soup of different deal contracts (each contract may have many versions) 25 | # Feature 26 | 27 | # Every time a new contract is uploaded to a deal folder in Google Drive: 28 | 29 | # If the folder hasn’t been processed yet, we want to try and build an organized index of documents/versions (best guess) 30 | # If the folder has already been processed, just deal with the new uploaded file and update the index 31 | 32 | class Client 33 | has_many :deals 34 | end 35 | 36 | class Deal 37 | has_many :documents 38 | end 39 | 40 | class Document 41 | has_many :versions 42 | end 43 | 44 | class Document::Version 45 | belongs_to :document, optional: true 46 | 47 | has_one_attached :file 48 | 49 | after_create_commit :consolidate 50 | 51 | def consolidate 52 | deal.consolidate deal.metadata 53 | end 54 | end 55 | 56 | class Document::Index 57 | end 58 | 59 | class Deals::UploadsController < ApplicationController 60 | def create 61 | Current.deal.process_batch files: params[:files] 62 | end 63 | end 64 | 65 | class Deal::Processor::Batch 66 | belongs_to :deal 67 | has_many :files 68 | 69 | def schedule_later 70 | 71 | end 72 | end 73 | 74 | class Deal::Processor::File 75 | belongs_to :batch 76 | delegate :deal, to: :batch 77 | 78 | after_create_commit :process_later 79 | 80 | has_one_attached :file 81 | 82 | enum status: %i[ pending processing processed errored ] 83 | 84 | def process 85 | unless processed? 86 | Document::Version.create! file:, deal: 87 | processed! 88 | end 89 | rescue 90 | errored! 91 | raise 92 | end 93 | 94 | def process_later 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /forum.rb: -------------------------------------------------------------------------------- 1 | # Forum prompt: 2 | # I'm reimagining an old-school forum app, that's a cross between GitHub Issues and Slack. 3 | # Users have one identity in the app and can be part of many forums. 4 | # Within a forum, there are channels that are either public or private. 5 | # Public channels show up to everyone and private channels must be joined. 6 | # Within channels, there are topics with posts. 7 | # Topics are always visible to people with channel access, but you can also subscribe to a topic to receive notifications related to updates. 8 | 9 | class User 10 | has_many :memberships 11 | has_many :forums, through: :memberships 12 | 13 | has_many :subscriptions 14 | 15 | def subscribe_to(record) 16 | subscriptions.create! record: 17 | end 18 | end 19 | 20 | class Membership 21 | belongs_to :user 22 | belongs_to :forum 23 | end 24 | 25 | class Forum 26 | has_many :memberships 27 | has_many :users, through: :memberships, after_add: :auto_enroll 28 | 29 | has_many :channels 30 | 31 | def auto_enroll(user) 32 | channels.unrestricted.enroll user 33 | end 34 | end 35 | 36 | class Forum::Channel 37 | belongs_to :forum 38 | has_many :topics 39 | 40 | has_many :enrollments 41 | 42 | before_create :build_enrollments, unless: :restricted? 43 | 44 | # name, description 45 | 46 | def build_enrollments 47 | forum.users.each do |user| 48 | enrollments.build user: 49 | end 50 | end 51 | end 52 | 53 | class Forum::Channel::Enrollment 54 | belongs_to :user 55 | belongs_to :channel 56 | 57 | # moderator? 58 | end 59 | 60 | @channel.enrollments.each do |enrollment| 61 | user.name + moderator_badge_for(enrollment) 62 | end 63 | 64 | class Forum::Topic 65 | belongs_to :channel 66 | has_many :posts 67 | end 68 | 69 | # app/models/forum/topic/post.rb 70 | class Forum::Topic::Post 71 | belongs_to :user 72 | belongs_to :topic 73 | delegate :channel, to: :topic 74 | 75 | after_create { user.subscribe_to topic } 76 | after_create_commit :broadcast_later 77 | 78 | def broadcast 79 | User::Subscription.where(record: [topic, channel, channel.forum], user: users).each do |subscription| 80 | subscription.create_broadcast_for self 81 | end 82 | end 83 | end 84 | 85 | class User::Subscription 86 | belongs_to :user 87 | belongs_to :forum 88 | delegated_type :record, types: %i[ Forum Channel Topic ] 89 | end 90 | 91 | class Users::SubscriptionsController 92 | def create 93 | Current.user.subscribe_to params[:id] 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /friend.rb: -------------------------------------------------------------------------------- 1 | # Make a First Ruby Friend app that automatically does the matching of mentors and mentees, 2 | # based on geography (local context and timezone), current level, mentor demographic preference. 3 | 4 | class User 5 | belongs_to :profile 6 | 7 | has_many :groups 8 | has_many :cohorts, through: :groups 9 | end 10 | 11 | class Program::Cohort 12 | # started_at ended_at 13 | has_many :groups 14 | end 15 | 16 | class Program::Cohort::Group 17 | has_many :participants 18 | end 19 | 20 | class Program::Cohort::Participant 21 | enum role: %i[ mentor mentee ] 22 | end 23 | 24 | class Program::IntentRequest 25 | belongs_to :cohort 26 | 27 | has_many :request_details 28 | has_rich_text :description 29 | end 30 | 31 | class Program::RequestDetail 32 | belongs_to :intent_request 33 | belongs_to :detail 34 | 35 | enum :kind, %i[ required preferred ] 36 | end 37 | 38 | class Profile::Detail 39 | end 40 | 41 | class Mentor 42 | belongs_to :user 43 | end 44 | 45 | class Mentee 46 | belongs_to :user 47 | end 48 | 49 | class Profile 50 | belongs_to :user 51 | 52 | # country 53 | # time_zone 54 | end 55 | -------------------------------------------------------------------------------- /joiner.rb: -------------------------------------------------------------------------------- 1 | # Riffed on joining a User, which would probably be in most databases 2 | # with tables in Memory in ActiveRecord. Based on the availability of cross 3 | # database joins starting in Rails 7, the :disable_joins flag creates separate 4 | # queries. 5 | # 6 | # I'll probably do another one soon joining straight into duckdb. 7 | # These are especially useful for agentic activity, where some data will always 8 | # be ephemeral. 9 | # 10 | # See this hack for running it. https://thoughtbot.com/blog/rails-runner 11 | # Based on https://github.com/kaspth/riffing-on-rails --see README 12 | # 13 | # Place this script in lib/joiner.rb or name similarly. 14 | # This assumes a User model in a rails app, with a different ActiveRecord connection 15 | # that the in memory one can join to. 16 | 17 | require "bundler/setup" 18 | require "active_record" 19 | require "action_controller" 20 | 21 | class MemoryRecord < ActiveRecord::Base 22 | self.abstract_class = true 23 | establish_connection( 24 | adapter: "sqlite3", 25 | database: ":memory:", # keeps everything in RAM 26 | pool: 1, 27 | timeout: 1000 28 | ) 29 | end 30 | 31 | class Limb < MemoryRecord 32 | belongs_to :body 33 | has_one :user, through: :body, disable_joins: true 34 | 35 | scope :dominant, ->(side) { where("name LIKE ?", "%" + side.to_s.titleize + "%") } 36 | scope :head, -> { where("name LIKE ?", "%Head%") } 37 | end 38 | 39 | class Body < MemoryRecord 40 | has_many :limbs 41 | belongs_to :user 42 | end 43 | 44 | MemoryRecord.connection.create_table :limbs do |t| 45 | t.references :body, null: false, index: true 46 | t.string :name, null: false 47 | end 48 | 49 | MemoryRecord.connection.create_table :bodies do |t| 50 | t.string :title, null: false 51 | t.text :content, null: false 52 | t.references :user, null: false, index: true 53 | end 54 | 55 | class User # this is going to add to the user in rails, these relations 56 | has_one :body 57 | has_many :limbs, through: :body, disable_joins: true 58 | 59 | end 60 | 61 | User.all.each do |user| 62 | body = Body.create!(title: "Homo Sapiens", content: "A standard body", user: ) 63 | limbs = ["Left Leg", "Right Leg", "Left Arm", "Right Arm", "Head"] 64 | limbs.each do 65 | Limb.create!(body:, name: _1) 66 | end 67 | end 68 | 69 | pp User.all.sample.limbs.map(&:name) 70 | 71 | binding.irb 72 | 73 | MemoryRecord.connection_pool.disconnect! 74 | -------------------------------------------------------------------------------- /mix.rb: -------------------------------------------------------------------------------- 1 | # Spotify Mix Playlists: Assuming we have a history of played songs for a user, 2 | # we have song recommendations via nearest neighbor search, 3 | # and we have categorizations (genre, mood, era, instrumental/vocal, cultural/regional, theme), 4 | # let system admins create mix templates based on music categorizations 5 | # and then generate refreshable custom playlists for each user. 6 | 7 | class User 8 | has_many :playlists 9 | has_one :history 10 | end 11 | 12 | class History 13 | has_many :listens 14 | has_many :tracks, through: :listens 15 | end 16 | 17 | class History::Listen 18 | belongs_to :history 19 | belongs_to :track 20 | end 21 | 22 | class Track 23 | has_many :categorizations 24 | has_many :categories, through: :categorizations 25 | end 26 | 27 | track.inner_joins(Track::Category.genres).nearest(100) 28 | 29 | Track::Category.genres.where(value: ["pop", "hiphop"]) 30 | Track::Category.eras.where(value: ["80s", "90s"]) 31 | 32 | class Track::Category 33 | has_many :categorizations 34 | has_many :tracks, through: :categorizations 35 | 36 | belongs_to :details 37 | end 38 | 39 | class Track::Category::Categorization 40 | belongs_to :track 41 | belongs_to :category 42 | end 43 | 44 | class Playlist 45 | end 46 | 47 | class Mix::Template 48 | has_many :categories 49 | 50 | def build_for(user) 51 | from_own_history = user.history.tracks.ordered_by_popularity.joins(:categories).where(categories:).limit(100).flat_map do |track| 52 | [track, track.nearest(10)] 53 | end.uniq.first(100) 54 | 55 | if from_own_history >= 100 56 | from_own_history 57 | else 58 | Track.ordered_by_popularity.joins(:categories).where(categories:).limit(100).flat_map do |track| 59 | [track, track.nearest(10)] 60 | end.including(from_own_history).uniq.first(100) 61 | end 62 | end 63 | end 64 | 65 | class Mix::Build 66 | belongs_to :template 67 | belongs_to :user 68 | 69 | has_many :links 70 | has_many :tracks, through: :links 71 | 72 | def regenerate 73 | update! tracks: template.build_for(user) 74 | end 75 | end 76 | 77 | class Mix::Build::Link 78 | belongs_to :build 79 | belongs_to :track 80 | end 81 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Riffing on Rails 2 | 3 | When you're working on a new feature, it can be tough to break through how to actually build it. A common path is to start somewhere, run some Rails scaffolds and migrate the database. Then a while later when you finally get the problem, you're too far in — inertia takes a hold and sunk cost soon follows, "eh, I don't want to rip all that out now". 4 | 5 | Wouldn't it be great if there was a different way? 6 | 7 | ### Riffing: An alternative you've never seen before 8 | 9 | Riffing is an altogether different approach to software design that has more in common with art or creative acts. We're trying to engage our brain in a different way to come up with the names and structure we didn't know we needed by tapping into flow states. 10 | 11 | Riffing is a code-based method for problem solving. It's done in a blank file where you keep adding, removing or refining a sketch of Ruby code to prove out your design. 12 | 13 | This combines a focus on top-down design with listening to what the budding implementation is telling you — and if the implementation reveals an issue with your design, you now know you need to change your design early. 14 | 15 | > [!TIP] 16 | > If you've heard of fat-marker sketches for UI design (so people don't get too bogged down in the implementation details), this is the code equivalent. 17 | 18 | So we're looking to get as much code design feedback as quickly as we can and being able to riff allows you to: 19 | 20 | - Move implementation insights you'd have towards the tail end of a project all the way upfront 21 | - By listening to the code, you can surfuce known unknowns and unknowns unknowns — and raise them with stakeholders on day one of a project 22 | - Unlock naming and code structure that you wouldn't have gotten from just wearing your "engineering" hat 23 | 24 | It's best done in a session that lasts 30-60 min either alone or with partners. After a session, take a break and come back to it or sleep on it. If you're hitting diminishing returns that's also a sign to stop. 25 | 26 | ### See riffing in action on YouTube 27 | 28 | [@jeremysmithco](https://github.com/jeremysmithco) and [@kaspth](https://github.com/kaspth) have done a few sessions to demonstrate this technique. 29 | 30 | [Here's the session where dispatcher.rb came about](https://www.youtube.com/watch?v=qQ0BxKFFX9Q). With special guest, @tcannonfodder! 31 | 32 | [Here's the session where friend.rb and rss.rb came about](https://www.youtube.com/watch?v=NjzzVMnkEo0). We're also showing how to make scratch.rb runnable so you can test out your interfaces in a console. 33 | 34 | [Here's the session where mix.rb came about](https://www.youtube.com/watch?v=i1MM2EOniPg) 35 | 36 | We didn't record when we did scratch.rb but it happened pretty much the same way. 37 | 38 | ### RailsConf talk 39 | 40 | [@kaspth](https://github.com/kaspth) did a RailsConf talk about their general experiences with riffing. 41 | 42 | [Video](https://www.youtube.com/watch?v=vH-mNygyXs0) — [Slides](https://speakerdeck.com/kaspth/railsconf-2024-riffing-on-rails-sketch-your-way-to-better-designed-code) 43 | 44 | ### BikeShed episode on Domain Modeling & Riffing 45 | 46 | [@kaspth](https://github.com/kaspth) went on the Bike Shed here https://bikeshed.thoughtbot.com/433 47 | 48 | ### A write up intro on Riffing 49 | 50 | https://buttondown.com/kaspth/archive/my-intro-to-riffing-on-the-bike-shed-and-railsconf-talk/ 51 | -------------------------------------------------------------------------------- /reports.rb: -------------------------------------------------------------------------------- 1 | # Recurring Reports: Allow users to manage a report builder for recurring reports with questions of various types, 2 | # given out to a set of users or organizations that must complete the report at regular intervals (weekly, monthly, yearly). 3 | # Keep track of report completion status for each round of reporting, and make it so answers can be aggregated and compared, 4 | # both across users/orgs and over time. Allow questions to be added or removed over time. 5 | 6 | class Report::Template 7 | has_many :questions 8 | 9 | belongs_to :timing 10 | end 11 | 12 | class Report::Delivery 13 | belongs_to :report 14 | has_many :reply_requests 15 | 16 | # delivered_at 17 | 18 | def deliver_from(template) 19 | template.users.each do |user| 20 | 21 | end 22 | end 23 | end 24 | 25 | class Reports::Templates::Questions::ReplacementsController 26 | def create 27 | @template = Report.find(params[:report_id]).template 28 | @question = @template.questions.find(params[:question_id]) 29 | 30 | ActiveRecord::Base.transaction do 31 | new_question = @template.questions.create!(params[:template].permit!) 32 | 33 | Delivery::Submission.where(template_question: @question).update_all template_question_id: new_question.id 34 | @question.destroy! 35 | end 36 | end 37 | end 38 | 39 | class Report::Template::Question 40 | has_many :delivery_submissions 41 | end 42 | 43 | class Report::Delivery::Question 44 | belongs_to :template_question 45 | end 46 | 47 | class Report::Delivery::Submission 48 | belongs_to :user 49 | has_many :question_submissions 50 | # has_many :questions, through: :question_submissions 51 | 52 | def fulfilled? 53 | question_submissions.all?(&:fullfilled?) 54 | end 55 | end 56 | 57 | class Report::Question::Submission 58 | belongs_to :delivery_submission 59 | belongs_to :delivery_question 60 | 61 | def fulfilled? = fulfilled_at? 62 | end 63 | 64 | class Report::Template::Timing 65 | enum :kind, %i[ weekly monthly yearly ] 66 | 67 | def next_window_from(past_delivery) 68 | case kind 69 | when :weekly then past_delivery.last_sent_at + 1.week 70 | when :monthly then 1.month.from_now 71 | when :yearly then 1.year.from_now 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /rss.rb: -------------------------------------------------------------------------------- 1 | # RSS Feed Reader: Users subscribe to RSS and Atom feeds, and get a list of posts they can read and favorite. 2 | 3 | class User 4 | has_many :subscriptions 5 | has_many :feeds, through: :subscriptions 6 | 7 | belongs_to :timeline 8 | 9 | has_many :favorites 10 | 11 | def favorite(item) 12 | favorites.create_or_find_by! item: 13 | end 14 | 15 | def unfavorite(item) 16 | favorites.destroy_by item: 17 | end 18 | end 19 | 20 | class Subscription 21 | belongs_to :user 22 | belongs_to :feed 23 | end 24 | 25 | class Feed 26 | has_many :subscriptions 27 | has_many :users, through: :subscriptions 28 | 29 | # url 30 | 31 | def public? 32 | !personal? 33 | end 34 | 35 | def personal? 36 | password_details? 37 | end 38 | end 39 | 40 | class Feed::Post 41 | end 42 | 43 | class User::Timeline 44 | has_many :items 45 | end 46 | 47 | class User::Favorite 48 | belongs_to :user 49 | belongs_to :item 50 | end 51 | 52 | class User::Timeline::Item 53 | belongs_to :timeline 54 | belongs_to :post 55 | 56 | # accepted_at 57 | # rejected_at 58 | belongs_to :download, optional: true 59 | end 60 | 61 | class User::TimelinesController < ApplicationController 62 | def show 63 | @items = Current.user.timeline.items.order(:created_at) 64 | end 65 | end 66 | 67 | # app/views/user/timelines/show.html.erb 68 | @items.each do |item| 69 | if Current.user.favorite?(item) 70 | button_to unfavorite_item_path(item), method: :delete 71 | else 72 | button_to favorite_item_path(item) 73 | end 74 | end 75 | 76 | class User::Items::FavoritesController < ApplicationController 77 | def show 78 | end 79 | 80 | def create 81 | @item = Current.user.timeline.items.find(params[:id]) 82 | Current.user.favorite @item 83 | end 84 | 85 | def destroy 86 | @item = Current.user.timeline.items.find(params[:id]) 87 | Current.user.unfavorite @item 88 | end 89 | end 90 | 91 | 92 | resources :users do 93 | namespace :timeline do 94 | resources :favorites 95 | end 96 | end 97 | 98 | 99 | class Current < ActiveSupport::CurrentAttributes 100 | attribute :user 101 | end 102 | -------------------------------------------------------------------------------- /scratch.rb: -------------------------------------------------------------------------------- 1 | # You can run this locally from the `riff` clone directory: 2 | # bundle exec ruby scratch.rb 3 | # 4 | # 1. Spam post detection: Run a forum post through a series of configurable checks to give it a spam score, 5 | # and flag the post when it crosses the threshold, with details on what led to the score. 6 | 7 | require "bundler/setup" 8 | require "active_record" 9 | require "action_controller" 10 | 11 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 12 | 13 | ActiveRecord::Schema.define do 14 | create_table :posts do |t| 15 | t.references :user, null: false, index: true 16 | t.string :title, null: false 17 | t.text :content, null: false 18 | end 19 | 20 | create_table :users do |t| 21 | end 22 | end 23 | 24 | class ApplicationRecord < ActiveRecord::Base 25 | self.abstract_class = true 26 | end 27 | 28 | class Post < ApplicationRecord 29 | belongs_to :user 30 | end 31 | 32 | class User < ApplicationRecord 33 | has_many :posts 34 | end 35 | 36 | module Spam 37 | module Detectors 38 | def self.check(post) 39 | Check.new post, Abstract.detectors 40 | end 41 | 42 | class Check 43 | def initialize(post, detectors) 44 | @detectors = detectors.map { _1.new(post:) } 45 | end 46 | 47 | def score 48 | @detectors.sum(&:score) / @detectors.size 49 | end 50 | end 51 | 52 | class Abstract < Struct.new(:post, :max_hits, keyword_init: true) 53 | def initialize(post:, max_hits: 1) = super 54 | 55 | singleton_class.attr_reader :detectors 56 | @detectors = [] 57 | def self.inherited(detector) = detectors << detector 58 | 59 | def hits 60 | hit? ? 1 : 0 61 | end 62 | 63 | def score 64 | hits / max_hits.to_f 65 | end 66 | end 67 | 68 | module Content 69 | end 70 | 71 | module Account 72 | end 73 | end 74 | end 75 | 76 | class Spam::Detectors::Account::PostCount < Spam::Detectors::Abstract 77 | def hit? 78 | post.user.posts.where(created_at: 1.hour.ago..).count >= 50 79 | end 80 | end 81 | 82 | # The first check we did, just to start us off and then we continued from here. 83 | # We also had checks respond to `score` but ultimately moved on to hits/max_hits though I don't quite remember the reasoning now. 84 | class Spam::Detectors::Content::FirstPost < Spam::Detectors::Abstract 85 | def hit? 86 | post.content == "My first post" 87 | end 88 | end 89 | 90 | class Spam::Detectors::Content::LinkCount < Spam::Detectors::Abstract 91 | def hits 92 | content_links.size 93 | end 94 | 95 | def max_hits 96 | 10 97 | end 98 | 99 | private 100 | 101 | def content_links 102 | post.content.scan /https?:.*? / 103 | end 104 | end 105 | 106 | class Spam::Detectors::Content::Dictionary < Spam::Detectors::Abstract 107 | def initialize(post, words) 108 | super 109 | @words = words 110 | end 111 | 112 | def hits 113 | content_words.uniq.size 114 | end 115 | 116 | def max_hits 117 | words.size 118 | end 119 | 120 | private 121 | 122 | def content_words 123 | post.content.scan Regexp.new(@words.join("|")) 124 | end 125 | end 126 | 127 | class ApplicationController < ActionController::Base 128 | end 129 | 130 | class Post::SpamDetectionsController < ApplicationController 131 | def create 132 | @detection = Spam::Detectors.check @post 133 | end 134 | end 135 | 136 | user = User.create! 137 | post = Post.create! user:, title: "First", content: "Heyo" 138 | 139 | 140 | binding.irb 141 | --------------------------------------------------------------------------------