amatriain/feedbunch

View on GitHub
FeedBunch-app/lib/folder_manager.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

##
# This class has methods related to managing the relationships between feeds and folders.

class FolderManager

  ##
  # Returns feeds in the passed folder.
  #
  # Accepts as arguments:
  # - The folder from which feeds must be retrieved. This argument can be:
  #   - a Folder instance. Feeds in this folder will be returned. In this case folder must be owned by the passed user,
  # otherwise an error is raised
  #   - the special Folder::NO_FOLDER value. In this case feeds subscribed by the passed user which are not in any folder
  # will be returned
  # - The user for whom feeds will be retrieved.
  # - include_read (optional). A boolean argument that defaults to false. If this argument is true, all feeds in the
  # folder will be returned. If it's false, only feeds with unread entries will be returned.
  #
  # The returned feeds are guaranteed to be subscribed by the passed user.

  def self.folder_feeds(folder, user, include_read: false)
    if folder == Folder::NO_FOLDER
      feeds = Feed.arel_table
      folders = Folder.arel_table
      feed_subscriptions = FeedSubscription.arel_table
      feeds_folders = Arel::Table.new :feeds_folders

      feeds_in_folders_condition = feeds_folders.join(folders).on(folders[:id].eq(feeds_folders[:folder_id])).
                                where(folders[:user_id].eq(user.id)).
                                where(feeds_folders[:feed_id].eq(feeds[:id])).
                              project(feeds_folders[Arel.star])

      if include_read
        subscribed_feeds_sql = feeds.join(feed_subscriptions).on(feeds[:id].eq(feed_subscriptions[:feed_id])).
                                  where(feed_subscriptions[:user_id].eq(user.id)).
                                project(feeds[Arel.star])
      else
        subscribed_feeds_sql = feeds.join(feed_subscriptions).on(feeds[:id].eq(feed_subscriptions[:feed_id])).
                                  where(feed_subscriptions[:user_id].eq(user.id)).
                                  where(feed_subscriptions[:unread_entries].gt(0)).
                                project(feeds[Arel.star])
      end

      feeds_not_in_folders_sql = subscribed_feeds_sql.where(feeds_in_folders_condition.exists.not)
                                  .order(feeds[:title])

      feeds_list = Feed.find_by_sql feeds_not_in_folders_sql.to_sql
    else
      # Validate that folder belongs to user
      if !user.folders.include? folder
        Rails.logger.error "User #{user.id} - #{user.email} tried to list feeds in folder #{folder.id} - #{folder.title} which he does not own"
        raise FolderNotOwnedByUserError.new
      end

      if include_read
        feeds_list = folder.feeds.order(:title)
      else
        feeds_list = folder.feeds.joins(:feed_subscriptions)
                      .where(feed_subscriptions: {user_id: user.id})
                      .where('feed_subscriptions.unread_entries > 0')
                      .order(:title)
      end
    end

    return feeds_list
  end

  ##
  # Move a feed into a folder, for a given user.
  #
  # Receives as arguments:
  #
  # - feed: mandatory. Feed to be moved.
  # - user: mandatory. User who is moving the feed. User must be subscribed to the feed and own the folder.
  # - folder: optional named argument. If present, moves the feed to this folder, which must be owned by
  # the passed user; in this case, ignores the folder_title argument.
  # Accepts the special value "none", which means that the feed will be removed from its current folder without moving
  # it to another one.
  # - folder_title: optional named argument. If present, and the folder argument is not present, create a new folder with this
  # title (owned by the passed user) and move the feed to it.
  #
  # Returns the folder instance to which the feed has been moved, or nil if "none" has been passed
  # in the "folder" argument.
  #
  # Raises a NotSubscribedError if the user is not subscribed to the feed.

  def self.move_feed_to_folder(feed, user, folder: nil, folder_title: nil)
    if !user.feeds.exists? feed.id
      Rails.logger.error "User #{user.id} - #{user.email} tried to change folder for feed #{feed.id} #{feed.fetch_url} to which he is not subscribed"
      raise NotSubscribedError.new
    end

    if folder.present? && folder != Folder::NO_FOLDER
      folder = move_feed_to_existing_folder feed, folder, user
    elsif  folder == Folder::NO_FOLDER
      folder = remove_feed_from_folder feed, user
    else
      folder = move_feed_to_new_folder feed, folder_title, user
    end

    return folder
  end

  #############################
  # PRIVATE CLASS METHODS
  #############################

  ##
  # Move a feed to an existing folder.
  #
  # Receives as arguments the feed, the folder and the user instance which is subscribed
  # to the feed and who owns the folder.
  #
  # If the feed was previously in another folder (owned by the same user), it is removed from that folder.
  # If there are no more feeds in that folder, it is deleted.
  #
  # If the method detects that the feed is being moved to the same folder it's already at, no action is
  # taken.
  #
  # Returns the folder instance to which the feed has been moved

  def self.move_feed_to_existing_folder(feed, folder, user)
    # Retrieve the current folder the feed is in, if any
    old_folder = feed.user_folder user

    if folder == old_folder
      Rails.logger.info "user #{user.id} - #{user.email} is trying to add feed #{feed.id} - #{feed.fetch_url} to folder #{folder.id} - #{folder.title}, but the feed is already in that folder"
    else
      Rails.logger.info "user #{user.id} - #{user.email} is adding feed #{feed.id} - #{feed.fetch_url} to folder #{folder.id} - #{folder.title}"
      folder.feeds << feed
    end

    return folder
  end
  private_class_method :move_feed_to_existing_folder

  ##
  # Create a new folder owned by the user, and move a feed to it.
  #
  # Receives as arguments the feed, the title of the new folder and the user who will own the folder.
  #
  # If the user already has a folder with the same title, raises a FolderAlreadyExistsError.
  #
  # If the feed was previously in another folder (owned by the same user), it is removed from that folder.
  # If there are no more feeds in that folder, it is deleted.
  #
  # Returns the folder instance to which the feed has been moved.

  def self.move_feed_to_new_folder(feed, folder_title, user)
    if user.folders.where(title: folder_title).present?
      Rails.logger.info "User #{user.id} - #{user.email} tried to create a new folder with title #{folder_title}, but it already has a folder with that title"
      raise FolderAlreadyExistsError.new
    end

    Rails.logger.info "Creating folder with title #{folder_title} for user #{user.id} - #{user.email}"
    folder = user.folders.create title: folder_title
    move_feed_to_existing_folder feed, folder, user
    return folder
  end
  private_class_method :move_feed_to_new_folder

  ##
  # Remove a feed from its current folder.
  #
  # Receives as arguments the feed and the user instance who is removing the feed. The user must
  # be subscribed to the feed.
  #
  # If there are no more feeds in the folder, it is deleted.
  #
  # Returns nil.

  def self.remove_feed_from_folder(feed, user)
    # Retrieve the current folder the feed is in, if any
    old_folder = feed.user_folder user

    if old_folder.present?
      Rails.logger.info "user #{user.id} - #{user.email} is removing feed #{feed.id} - #{feed.fetch_url} from folder #{old_folder.id} - #{old_folder.title}"
      feed.remove_from_folder user
    else
      Rails.logger.info "user #{user.id} - #{user.email} tried to remove feed #{feed.id} - #{feed.fetch_url} from its folder, but it's not in any folder"
    end

    return nil
  end
  private_class_method :remove_feed_from_folder
end