diff --git a/.github/workflows/ruby_on_rails.yml b/.github/workflows/ruby_on_rails.yml index 6f0ca02..58e4079 100644 --- a/.github/workflows/ruby_on_rails.yml +++ b/.github/workflows/ruby_on_rails.yml @@ -12,7 +12,7 @@ jobs: services: postgres: - image: postgres:11-alpine + image: postgres:15-alpine ports: - "5432:5432" env: diff --git a/app/controllers/instapins_controller.rb b/app/controllers/instapins_controller.rb new file mode 100644 index 0000000..88e026e --- /dev/null +++ b/app/controllers/instapins_controller.rb @@ -0,0 +1,68 @@ +class InstapinsController < ApplicationController + include RestrictedAccess + def index + scope = set_scope + + @instapins = Instapin.send(scope).order(id: :desc).page params[:page] + @total_instapins = Instapin.send(scope).count + end + + def show + @instapin = Instapin.find(params[:id]) + end + + def edit + @instapin = Instapin.find(params[:id]) + @instapin_text = JSON.parse(@instapin.stem)["post"] + end + + def update + @instapin = Instapin.find(params[:id]) + # Parse the JSON stored in the stem attribute + stem_json = JSON.parse(@instapin.stem) + + # Update the "instapin" value in the parsed JSON object + stem_json["post"] = params[:instapin][:stem] + + # Convert the JSON object back to a string and save it to the instapin's stem attribute + @instapin.stem = stem_json.to_json + + # Turn off invalid JSON since its now valid + @instapin.invalid_json = false + + # Save the InstaPin + if @instapin.save + redirect_to instapin_path(@instapin), notice: 'InstaPin was successfully updated.' + else + render :edit + end + end + + def approve + @instapin = Instapin.find(params[:id]) + @instapin.update(approved: true) + redirect_to instapin_path(@instapin) + end + + def disapprove + @instapin = Instapin.find(params[:id]) + @instapin.update(approved: false) + redirect_to instapin_path(@instapin) + end + + private + + def set_scope + if params[:scope] && params[:scope] == 'pending' + return 'needs_approval' + elsif params[:scope] && params[:scope] == 'approved' + return 'approved_instapins' + elsif params[:scope] && params[:scope] == 'denied' + return 'denied' + elsif params[:scope] && params[:scope] == 'published' + return 'published' + end + + 'all' + end +end diff --git a/app/jobs/assemble_job.rb b/app/jobs/assemble_job.rb index 1d49568..b9af75f 100644 --- a/app/jobs/assemble_job.rb +++ b/app/jobs/assemble_job.rb @@ -49,6 +49,9 @@ def perform(*_args) # [x] Create Tweets from Discussions Tweets::ProcessTwitterStemsJob.perform_now + + # [x] Create Instapins from Uploaded Discussions + Instapins::ProcessInstapinStemsJob.perform_now ensure # Unlock the job when finished lock.update(locked: false) diff --git a/app/jobs/instapins/make_stem_job.rb b/app/jobs/instapins/make_stem_job.rb new file mode 100644 index 0000000..59cfc68 --- /dev/null +++ b/app/jobs/instapins/make_stem_job.rb @@ -0,0 +1,80 @@ +class Instapins::MakeStemJob < ApplicationJob + queue_as :default + include SettingsHelper + + def perform(discussion:) + return if discussion.instapin.present? + + @client = OpenAI::Client.new + + prompts = discussion.story.sub_topic.prompts + story = discussion.story + + system_role = <<~SYSTEM_ROLE + #{s("prompts.#{prompts}.tweets.stem.system_role")} + SYSTEM_ROLE + + brief = <<~BRIEF + You have received the following story in JSON format: + + #{discussion.stem} + BRIEF + + question = <<~QUESTION + - You have received the following as a news story about `#{story.sub_topic.name}` and `#{story.tag.name}`. + - It is your job to write #{s("prompts.#{prompts}.tweets.stem.tone")} 400 character `post` about this story. + - You must include relevant 2 to 3 `hashtags` and 2 to 3 `emojis` into the `post`. + - The return result MUST be in JSON format in the following structure: + + ``` + { + post: "this is a post", + } + ``` + QUESTION + + messages = [ + { role: "system", content: system_role }, + { role: "user", content: brief }, + { role: "user", content: question } + ] + + invalid_json = true + counter = 0 + response = nil + + while invalid_json && counter < 6 + response = chat(messages:) + + counter += 1 + + break if response["error"].present? + + invalid_json = false if valid_json?(response["choices"][0]["message"]["content"]) + Rails.logger.debug "invalid_json: #{invalid_json} | counter: #{counter}" + end + + if response["error"].present? + Instapin.create(discussion:, processed: true, invalid_json:) + else + Instapin.create(discussion:, stem: response["choices"][0]["message"]["content"], processed: true, invalid_json:) + end + end + + def valid_json?(json_string) + JSON.parse(json_string) + true + rescue JSON::ParserError + false + end + + def chat(messages:) + @client.chat( + parameters: { + model: ENV['OPENAI_GPT_MODEL'], # Required. + messages:, + temperature: 0.7 + } + ) + end +end diff --git a/app/jobs/instapins/process_instapin_stems_job.rb b/app/jobs/instapins/process_instapin_stems_job.rb new file mode 100644 index 0000000..f630019 --- /dev/null +++ b/app/jobs/instapins/process_instapin_stems_job.rb @@ -0,0 +1,9 @@ +class Instapins::ProcessInstapinStemsJob < ApplicationJob + queue_as :default + + def perform(*args) + Discussion.ready_to_create_instapins.each do |discussion| + Instapins::MakeStemJob.perform_now(discussion:) + end + end +end diff --git a/app/jobs/instapins/publish_instapin_job.rb b/app/jobs/instapins/publish_instapin_job.rb new file mode 100644 index 0000000..b8802c2 --- /dev/null +++ b/app/jobs/instapins/publish_instapin_job.rb @@ -0,0 +1,87 @@ +class Instapins::PublishInstapinJob < ApplicationJob + queue_as :default + + def perform(discussion:) + return if discussion.instapin.uploaded + + story = discussion.story + instapin = discussion.instapin + + platforms = [] + platforms.push 'pinterest' if ENV['PINTEREST_ENABLED'] + platforms.push 'instagram' if ENV['INSTAGRAM_ENABLED'] + platforms.push 'facebook' if ENV['FACEBOOK_ENABLED'] + + instapin_text = JSON.parse(instapin.stem)['post'] + + if story.sub_topic.ai_disclaimer + instapin_text = "📟 AI Perspective: #{instapin_text}" + end + + max_characters = 400 + + # Truncate truncated_instapin_text to fit within the max_characters limit + # 31 characters are reserved for URL and a space + truncated_instapin_text = instapin_text.truncate(max_characters - 31, omission: '...') + + landscape_images = discussion.story.imaginations.where(aspect_ratio: :landscape).sample(3) + + landscape_image_urls = landscape_images.map do |landscape_image| + "https://ucarecdn.com/#{landscape_image.uploadcare.last['uuid']}/-/format/auto/-/quality/smart/-/resize/1440x/" + end + + + story_pro_discussion = StoryPro.get_discussion(discussion.story_pro_id) + discussion_slug = story_pro_discussion["entry"]["slug"] + category_slug = get_story_pro_category_slug(story) + + discussion_url = "#{ENV['STORYPRO_URL']}/discussions/#{category_slug}/#{discussion_slug}" + + full_instapin = "#{truncated_instapin_text} \n\n Read full story at: #{discussion_url}" + + carousel_items = landscape_image_urls.map do |landscape_url| + { + name: discussion.parsed_stem["title"].truncate(50), + link: discussion_url, + picture: landscape_url + } + end + + return if platforms.blank? + + + if platforms.include? 'instagram' + Ayrshare.post_message(post: full_instapin, platforms: ['instagram'] , media_urls: landscape_image_urls) + end + + if platforms.include? 'pinterest' + Ayrshare.post_pinterest_message(post: truncated_instapin_text, + platforms: ['pinterest'] , + media_urls: landscape_image_urls.sample(1), + pinterest_options: { + title: discussion.parsed_stem["title"].truncate(100), + link: discussion_url, + boardId: story.sub_topic.pinterest_board + }) + end + + # if platforms.include? 'facebook' + # Ayrshare.post_carousel(post: truncated_instapin_text, + # facebook_options: { + # carousel: { + # link: discussion_url, + # items: carousel_items + # } + # }) + # end + + + instapin.update(uploaded: true, published_at: Time.now.utc) + end + + def get_story_pro_category_slug(story) + story_pro_categories = StoryPro.get_categories + found_story_pro_category = story_pro_categories.find { |c| c["id"] == story.sub_topic.storypro_category_id } + found_story_pro_category['slug'] + end +end diff --git a/app/jobs/instapins/publish_job.rb b/app/jobs/instapins/publish_job.rb new file mode 100644 index 0000000..0b5c60c --- /dev/null +++ b/app/jobs/instapins/publish_job.rb @@ -0,0 +1,13 @@ +class Instapins::PublishJob < ApplicationJob + queue_as :default + MAX_INSTAPINS_PUBLISHED_AT_A_TIME = 1 + + def perform(*args) + settings = Setting.instance + return unless settings.within_publish_window? + + Discussion.ready_to_upload_instapins.sample(MAX_INSTAPINS_PUBLISHED_AT_A_TIME).each do |discussion| + Instapins::PublishInstapinJob.perform_now(discussion:) + end + end +end diff --git a/app/jobs/tweets/publish_tweet_job.rb b/app/jobs/tweets/publish_tweet_job.rb index c6b4141..d6d36bd 100644 --- a/app/jobs/tweets/publish_tweet_job.rb +++ b/app/jobs/tweets/publish_tweet_job.rb @@ -10,7 +10,6 @@ def perform(discussion:) platforms = [] platforms.push 'twitter' if ENV['TWITTER_ENABLED'] platforms.push 'linkedin' if ENV['LINKEDIN_ENABLED'] - platforms.push 'pinterest' if ENV['PINTEREST_ENABLED'] platforms.push 'facebook' if ENV['FACEBOOK_ENABLED'] tweet_text = JSON.parse(tweet.stem)['tweet'] @@ -20,10 +19,9 @@ def perform(discussion:) end max_characters = 280 - auto_hashtag = false - # Truncate tweet_text to fit within the MAX_CHARACTERS limit - # 28 characters are reserved for URL and a space + # Truncate tweet_text to fit within the max_characters limit + # 31 characters are reserved for URL and a space truncated_tweet_text = tweet_text.truncate(max_characters - 31, omission: '...') card_image = tweet.discussion.story.imaginations.where(aspect_ratio: :card).sample(1) @@ -39,8 +37,7 @@ def perform(discussion:) return if platforms.blank? - # Ayrshare.post_message(post: full_tweet, platforms: , media_urls: [card_image_url], auto_hashtag:) - Ayrshare.post_plain_message(post: full_tweet, platforms:, auto_hashtag:) + Ayrshare.post_plain_message(post: full_tweet, platforms:) tweet.update(uploaded: true, published_at: Time.now.utc) end diff --git a/app/jobs/update_settings_job.rb b/app/jobs/update_settings_job.rb index c7dac60..ad690a7 100644 --- a/app/jobs/update_settings_job.rb +++ b/app/jobs/update_settings_job.rb @@ -28,6 +28,7 @@ def perform(*args) max_stories_per_day: sub_topic["max_stories_per_day"], storypro_category_id: sub_topic["storypro_category_id"], storypro_user_id: sub_topic["storypro_user_id"], + pinterest_board: sub_topic["pinterest_board"], ai_disclaimer: sub_topic["ai_disclaimer"], prompts: sub_topic["prompts"], active: true) diff --git a/app/lib/ayrshare.rb b/app/lib/ayrshare.rb index acaf9e6..3ec5713 100644 --- a/app/lib/ayrshare.rb +++ b/app/lib/ayrshare.rb @@ -19,10 +19,10 @@ def self.configure yield(configuration) end - def self.post_plain_message(post:, platforms:, auto_hashtag:) + def self.post_plain_message(post:, platforms:) url = "https://app.ayrshare.com/api/post" headers = { 'Authorization' => "Bearer #{configuration.api_key}" } - body = { post:, platforms:, autoHashtag: auto_hashtag } + body = { post:, platforms: } response = HTTParty.post(url, headers:, body:) @@ -31,10 +31,34 @@ def self.post_plain_message(post:, platforms:, auto_hashtag:) JSON.parse(response.body) end - def self.post_message(post:, platforms:, auto_hashtag:, media_urls: []) + def self.post_pinterest_message(post:, platforms: ['pinterest'], media_urls: [], pinterest_options: {}) url = "https://app.ayrshare.com/api/post" headers = { 'Authorization' => "Bearer #{configuration.api_key}" } - body = { post:, platforms:, mediaUrls: [media_urls], autoHashtag: auto_hashtag } + body = { post:, platforms:, mediaUrls: [media_urls], pinterestOptions: pinterest_options } + + response = HTTParty.post(url, headers:, body:) + + raise "Error: #{response.code} - #{response.body}" unless response.code == 200 + + JSON.parse(response.body) + end + + def self.post_carousel(post:, facebook_options: {}) + url = "https://app.ayrshare.com/api/post" + headers = { 'Authorization' => "Bearer #{configuration.api_key}" } + body = { post:, platforms: ['facebook'], faceBookOptions: facebook_options } + + response = HTTParty.post(url, headers:, body:) + + raise "Error: #{response.code} - #{response.body}" unless response.code == 200 + + JSON.parse(response.body) + end + + def self.post_message(post:, platforms:, media_urls: []) + url = "https://app.ayrshare.com/api/post" + headers = { 'Authorization' => "Bearer #{configuration.api_key}" } + body = { post:, platforms:, mediaUrls: [media_urls] } response = HTTParty.post(url, headers:, body:) diff --git a/app/models/discussion.rb b/app/models/discussion.rb index f030ef6..d99fab9 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -33,6 +33,7 @@ class Discussion < ApplicationRecord belongs_to :story has_one :tweet, dependent: :destroy + has_one :instapin, dependent: :destroy # used by the `discussions/publish_job` to filter discussions ready to be published scope :ready_to_upload, lambda { @@ -64,6 +65,24 @@ class Discussion < ApplicationRecord .where(tweets: { uploaded: false, invalid_json: false, approved: true }) } + # used by the `instapins/process_instapin_stems_job` to decide which instapins are ready to be created + scope :ready_to_create_instapins, lambda { + joins(story: :sub_topic) + .where(uploaded: true, sub_topics: { active: true }) + .joins(:tweet) + .where(tweets: { uploaded: true, invalid_json: false, approved: true }) + .left_outer_joins(:instapin) + .where(instapins: { discussion_id: nil}) + } + + # used by the `instapins/publish_instapin_job` to decide which tweets are ready to be published + scope :ready_to_upload_instapins, lambda { + joins(story: :sub_topic) + .where(uploaded: true) + .joins(:instapin) + .where(instapins: { uploaded: false, invalid_json: false, approved: true }) + } + # used by `discussions_controller` for filtering scope :valid_discussions, -> { where(invalid_json: false) } diff --git a/app/models/instapin.rb b/app/models/instapin.rb new file mode 100644 index 0000000..149d746 --- /dev/null +++ b/app/models/instapin.rb @@ -0,0 +1,30 @@ +# == Schema Information +# +# Table name: instapins +# +# id :bigint not null, primary key +# discussion_id :bigint not null +# stem :text +# processed :boolean default(FALSE) +# invalid_json :boolean default(FALSE) +# uploaded :boolean default(FALSE) +# approved :boolean +# published_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# +class Instapin < ApplicationRecord + belongs_to :discussion + + # used by `instapins_controller` for filtering and `page_controller` for dashboard logic + scope :needs_approval, -> { where(approved: nil, uploaded: false) } + + # used by `instapins_controller` for filtering and `page_controller` for dashboard logic + scope :approved_instapins, -> { where(approved: true) } + + # used by `instapins_controller` for filtering and `page_controller` for dashboard logic + scope :denied, -> { where(approved: false) } + + # used by `instapins_controller` for filtering and `page_controller` for dashboard logic + scope :published, -> { where(uploaded: true) } +end diff --git a/app/models/sub_topic.rb b/app/models/sub_topic.rb index 56c1eed..ac79277 100644 --- a/app/models/sub_topic.rb +++ b/app/models/sub_topic.rb @@ -15,6 +15,7 @@ # max_stories_per_day :integer # ai_disclaimer :boolean default(FALSE) # active :boolean default(TRUE) +# pinterest_board :bigint # class SubTopic < ApplicationRecord diff --git a/app/views/instapins/_instapin.html.slim b/app/views/instapins/_instapin.html.slim new file mode 100644 index 0000000..3ee7daf --- /dev/null +++ b/app/views/instapins/_instapin.html.slim @@ -0,0 +1,31 @@ +- frame_id = dom_id(instapin, "text_turbo_frame") +=r ux.segment '!mt-10 !mb-20 !bg-zinc-100 !border-4 !border-zinc-200 !shadow-sm !px-10 !py-10 overflow-x-hidden' + =r ux.code + =r ux.div 'text-right' + = link_to 'Show Discussion', discussion_path(instapin.discussion) + + =r ux.strong 'text-blue-800 !mb-80' + = "Tag: #{instapin.discussion.story.tag.name}" + =r ux.div 'mt-5 mb-5' + + =r ux.divider + =r ux.div 'mt-0' + = turbo_frame_tag frame_id + =r ux.code + - unless instapin.invalid_json + - images = instapin.discussion.story.imaginations.where(aspect_ratio: :landscape) + .images.mb-5 + - images.each do |image| + = image_tag "https://ucarecdn.com/#{image.uploadcare.last['uuid']}/-/format/auto/-/quality/smart/-/resize/300x/", class: 'mr-5' + + = link_to edit_instapin_path(instapin), class: 'no-underline' + = highlight_hashtags(JSON.parse(instapin.stem)['post']) + - else + .text-red-700 + | We could not generate the instapin automatically. Please click `edit` to manually create it. + + div.mt-10.flex.justify-between + = link_to 'Edit InstaPin', edit_instapin_path(instapin) + div + = link_to '👍 Approve InstaPin', approve_instapin_path(instapin), class: "mr-5 no-underline hover:text-green-700 #{instapin.approved? ? 'font-bold text-green-700' : ''}" + = link_to '👎 Disapprove InstaPin', disapprove_instapin_path(instapin), class: "no-underline hover:text-red-700 #{instapin.approved == false ? 'font-bold text-red-700' : '' }" diff --git a/app/views/instapins/edit.html.slim b/app/views/instapins/edit.html.slim new file mode 100644 index 0000000..30049be --- /dev/null +++ b/app/views/instapins/edit.html.slim @@ -0,0 +1,23 @@ +- frame_id = dom_id(@instapin, "text_turbo_frame") += turbo_frame_tag frame_id + = form_with(model: @instapin, url: instapin_path(@instapin), method: :patch, local: true) do |form| + - if @instapin.errors.any? + div id="error_explanation" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" + h2 + = pluralize(@instapin.errors.count, "error") + | prohibited this instapin from being saved: + ul class="list-disc list-inside" + - @instapin.errors.full_messages.each do |message| + li class="text-sm"= message + + .field.flex.flex-col.mb-5 data-controller="character-counter" data-character-counter-countdown-value="true" + = form.text_area :stem, value: @instapin_text, class: 'form-input mt-1 block w-full rounded-md bg-gray-50 p-2 border-gray-300', + rows: 4, maxlength: 400, value: @instapin_text, data: {'character-counter-target': 'input'} + + p + strong data-character-counter-target="counter" + span.ml-2 characters remaining in this InstaPin. + + .actions.flex.justify-between + = form.submit class: 'py-2 px-4 bg-blue-500 text-white font-semibold rounded hover:bg-blue-700 cursor-pointer', value: 'Update InstaPin' + = button_to "Cancel", @instapin, class: 'py-2 px-4 bg-red-500 text-white font-semibold rounded hover:bg-red-700 cursor-pointer' \ No newline at end of file diff --git a/app/views/instapins/index.html.slim b/app/views/instapins/index.html.slim new file mode 100644 index 0000000..544177b --- /dev/null +++ b/app/views/instapins/index.html.slim @@ -0,0 +1,15 @@ +=r ux.container + =r ux.row + =r ux.column width: 16 + =r ux.h2 'center aligned !font-mono' + | InstaPins + =r ux.label text: @total_instapins, class: 'circular !bg-black !text-white' + + =r ux.div 'text-center' + = paginate @instapins + + - @instapins.each do |instapin| + = render 'instapin', instapin: instapin + + =r ux.div 'text-center' + = paginate @instapins diff --git a/app/views/instapins/show.html.slim b/app/views/instapins/show.html.slim new file mode 100644 index 0000000..2eff035 --- /dev/null +++ b/app/views/instapins/show.html.slim @@ -0,0 +1 @@ += render 'instapin', instapin: @instapin \ No newline at end of file diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 9bfcc01..434258e 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -90,6 +90,26 @@ html lang="en" = link_to tweets_path, class: "item" | All + =r ux.item class: "dropdown #{dropdown_active?(instapins_path)}", ui: :on + =r ux.icon 'instagram' + | InstaPins + =r ux.icon 'dropdown' + =r ux.menu + = link_to instapins_path(scope: :pending), class: "item" + | Pending + =r ux.divider + = link_to instapins_path(scope: :approved), class: "item" + | Approved + =r ux.divider + = link_to instapins_path(scope: :denied), class: "item" + | Denied + =r ux.divider + = link_to instapins_path(scope: :published), class: "item" + | Published + =r ux.divider + = link_to instapins_path, class: "item" + | All + =r ux.div 'item' =r ux.menu class: 'right', only: :mobile diff --git a/app/views/tweets/edit.html.slim b/app/views/tweets/edit.html.slim index 613a97b..7aa36ff 100644 --- a/app/views/tweets/edit.html.slim +++ b/app/views/tweets/edit.html.slim @@ -19,5 +19,5 @@ span.ml-2 characters remaining in this tweet. .actions.flex.justify-between - = form.submit class: 'py-2 px-4 bg-blue-500 text-white font-semibold rounded hover:bg-blue-700' - = button_to "Cancel", @tweet, class: 'py-2 px-4 bg-red-500 text-white font-semibold rounded hover:bg-red-700' \ No newline at end of file + = form.submit class: 'py-2 px-4 bg-blue-500 text-white font-semibold rounded hover:bg-blue-700 cursor-pointer' + = button_to "Cancel", @tweet, class: 'py-2 px-4 bg-red-500 text-white font-semibold rounded hover:bg-red-700 cursor-pointer' \ No newline at end of file diff --git a/app/views/tweets/index.html.slim b/app/views/tweets/index.html.slim index 0e23eeb..612aeb0 100644 --- a/app/views/tweets/index.html.slim +++ b/app/views/tweets/index.html.slim @@ -2,7 +2,7 @@ =r ux.row =r ux.column width: 16 =r ux.h2 'center aligned !font-mono' - | Tweets + | CrossTweets =r ux.label text: @total_tweets, class: 'circular !bg-black !text-white' =r ux.div 'text-center' diff --git a/config/routes.rb b/config/routes.rb index 9c670b1..38dcf9d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,6 +26,14 @@ get 'disapprove' end end + + resources :instapins, only: %i[index edit show update] do + member do + get 'approve' + get 'disapprove' + end + end + resources :imaginations, only: %i[index] resources :tags, only: %i[index] diff --git a/config/simple_scheduler.yml b/config/simple_scheduler.yml index 4e5c482..9699cae 100644 --- a/config/simple_scheduler.yml +++ b/config/simple_scheduler.yml @@ -13,6 +13,11 @@ tweet_task: every: '1.hours' at: "*:15" +instapin_task: + class: 'Instapins::PublishJob' + every: '2.hours' + at: "*:15" + assembly_task: class: 'AssembleJob' every: '30.minutes' diff --git a/db/migrate/20230729183024_create_instapins.rb b/db/migrate/20230729183024_create_instapins.rb new file mode 100644 index 0000000..1cdde4f --- /dev/null +++ b/db/migrate/20230729183024_create_instapins.rb @@ -0,0 +1,15 @@ +class CreateInstapins < ActiveRecord::Migration[7.0] + def change + create_table :instapins do |t| + t.references :discussion, null: false, foreign_key: true + t.text :stem + t.boolean :processed, default: false + t.boolean :invalid_json, default: false + t.boolean :uploaded, default: false + t.boolean :approved + t.datetime :published_at + + t.timestamps + end + end +end diff --git a/db/migrate/20230730172437_add_board_id_to_subtopics.rb b/db/migrate/20230730172437_add_board_id_to_subtopics.rb new file mode 100644 index 0000000..45636af --- /dev/null +++ b/db/migrate/20230730172437_add_board_id_to_subtopics.rb @@ -0,0 +1,5 @@ +class AddBoardIdToSubtopics < ActiveRecord::Migration[7.0] + def change + add_column :sub_topics, :pinterest_board, :bigint, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 27a3a43..a46f97f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,9 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_07_27_021629) do +ActiveRecord::Schema[7.0].define(version: 2023_07_30_172437) do # These are extensions that must be enabled in order to support this database - enable_extension "pgcrypto" + enable_extension "pg_stat_statements" enable_extension "plpgsql" create_table "assignments", force: :cascade do |t| @@ -94,6 +94,19 @@ t.jsonb "uploadcare" end + create_table "instapins", force: :cascade do |t| + t.bigint "discussion_id", null: false + t.text "stem" + t.boolean "processed", default: false + t.boolean "invalid_json", default: false + t.boolean "uploaded", default: false + t.boolean "approved" + t.datetime "published_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["discussion_id"], name: "index_instapins_on_discussion_id" + end + create_table "locks", force: :cascade do |t| t.string "name", null: false t.boolean "locked", default: false @@ -146,6 +159,7 @@ t.integer "max_stories_per_day" t.boolean "ai_disclaimer", default: false t.boolean "active", default: true + t.bigint "pinterest_board" t.index ["topic_id"], name: "index_sub_topics_on_topic_id" end @@ -215,6 +229,7 @@ add_foreign_key "feed_items", "feeds" add_foreign_key "feeds", "sub_topics" add_foreign_key "images", "stories" + add_foreign_key "instapins", "discussions" add_foreign_key "stories", "sub_topics" add_foreign_key "story_tags", "stories" add_foreign_key "story_tags", "tags" diff --git a/spec/factories/sub_topics.rb b/spec/factories/sub_topics.rb index a9ea64c..42aaadc 100644 --- a/spec/factories/sub_topics.rb +++ b/spec/factories/sub_topics.rb @@ -15,6 +15,7 @@ # max_stories_per_day :integer # ai_disclaimer :boolean default(FALSE) # active :boolean default(TRUE) +# pinterest_board :bigint # FactoryBot.define do factory :sub_topic do