apcdatanalytics/rets-rabbit

Display real estate listings in your craft site in a simple and intuitive way.

1.1.0 2019-09-25 12:33 UTC

This package is auto-updated.

Last update: 2024-09-18 02:39:20 UTC


README

This plugin allows you to connect to the Rets Rabbit API(v2) in order to display your listings in a clean and intuitive way.

Installation

composer require apcdatanalytics/rets-rabbit

Requirements

The Rets Rabbit plugin requires at least php 7.0 in accordance with minimum Craft 3 PHP requirements.

Documentation

You can interact with the Rets Rabbit API through the PropertiesVariable & OpenHousesVariable.

Open Houses

  1. craft.retsRabbit.openHouses.find - Single open house lookup
  2. craft.retsRabbit.openHouses.query - Run a raw RESO query

find(int $id, object $resoParams, bool $useCache = false, int $cacheDuration)

$id - The MLS id of the open house you want to fetch from the API.

$resoParams - You may pass valid RESO parameters to help filter the API results for a single open house. This can help speed up the response time if you specifically select the fields you will need from the API by using the $select parameter.

$useCache - Specify if you want the results cached.

$cacheDuration - Specify how long you would like the results cached for in seconds. The default is one hour.

{% set viewModel = craft.retsRabbit.openHouses.find('OpenHouseId', {'$select': 'OpenHouseId'}, true) %}

{% if viewModel.hasErrors() %}
    {# An error occurred, let the user know #}
{% elseif not viewModel.hasData() %}
    {# No data returned from request #}
{% else %}
    {% set openHouse = viewModel.data %}
    {{openHouse.OpenHouseId}}
{% endif %}

query(object $resoParams, bool $useCache = false, int $cacheDuration)

$resoParams - You may pass valid RESO parameters to help filter the API results for a single open house. This can help speed up the response time if you specifically select the fields you will need from the API by using the $select parameter.

$useCache - Specify if you want the results cached.

$cacheDuration - Specify how long you would like the results cached for in seconds. The default is one hour.

{% set viewModel = craft.retsRabbit.openHouses.query({
    '$select': 'OpenHouseId, OpenHouseDate, OpenHouseStartTime, OpenHouseEndTime',
    '$top': 12
}) %}

{% if viewModel.hasErrors() %}
    {# An error occurred #}
{% elseif not viewModel.hasData() %}
    {# No data returned in response #}
{% else %}
    {% set openHouses = viewModel.data %}
    {% for openHouse in openHouses %}
        <div class="card">
            <div class="card-header">
                {{openHouse.OpenHouseId}}
            </div>
            <div class="card-content">
                <div>
                    <date>{{ openHouse.OpenHouseDate }}</date>
                </div>
                <div>
                    <date>{{ openHouse.OpenHouseStartTime }}</date>
                </div>
                <div>
                    <date>{{ openHouse.OpenHouseEndTime }}</date>
                </div>
            </div>
        </div>
    {% endfor %}
{% endif %}

Properties

  1. craft.retsRabbit.properties.find - Single listing lookup
  2. craft.retsRabbit.properties.query - Run a raw RESO query
  3. craft.retsRabbit.properties.search - Perform a search using a saved query from a search form.

find(int $id, object $resoParams, bool $useCache = false, int $cacheDuration)

$id - The MLS id of the property you want to fetch from the API.

$resoParams - You may pass valid RESO parameters to help filter the API results for a single listing. This can help speed up the response time if you specifically select the fields you will need from the API by using the $select parameter.

$useCache - Specify if you want the results cached.

$cacheDuration - Specify how long you would like the results cached for in seconds. The default is one hour.

{% set viewModel = craft.retsRabbit.properties.find('123abc', {'$select': 'ListingId, ListPrice'}, true) %}

{% if viewModel.hasErrors() %}
    {# An error occurred, let the user know #}
{% elseif not viewModel.hasData() %}
    {# No data returned from request #}
{% else %}
    {% set listing = viewModel.data %}
    {{listing.ListingId}}
{% endif %}

query(object $resoParams, bool $useCache = false, int $cacheDuration)

$resoParams - You may pass valid RESO parameters to help filter the API results for a single listing. This can help speed up the response time if you specifically select the fields you will need from the API by using the $select parameter.

$useCache - Specify if you want the results cached.

$cacheDuration - Specify how long you would like the results cached for in seconds. The default is one hour.

{% set viewModel = craft.retsRabbit.properties.query({
    '$select': 'ListingId, ListPrice, PublicRemarks, StateOrProvince, City',
    '$filter': 'ListPrice ge 150000 and ListPrice le 175000 and BedroomsTotal ge 3',
    '$orderby': 'ListPrice',
    '$top': 12
}) %}

{% if viewModel.hasErrors() %}
    {# An error occurred #}
{% elseif not viewModel.hasData() %}
    {# No data returned in response #}
{% else %}
    {% set listings = viewModel.data %}
    {% for listing in listings %}
        <div class="card">
            <div class="card-header">
                {{listing.ListingId}}
            </div>
            <div class="card-content">
                {{listing.ListPrice}}
            </div>
        </div>
    {% endfor %}
{% endif %}

search(int $id, object $overrides, bool $useCache = false, int $cacheDuration)

$id - The id of the saved search parameters usually pulled from a url segment.

$overrides - You may pass in the following RESO parameters to help tailor your query search: $select, $orderby, $top.

$useCache - Specify if you want the results cached.

$cacheDuration - Specify how long you would like the results cached for in seconds. The default is one hour.

{# Results URL (for example): /search/results/4 #}
{% set searchId = craft.app.request.getSegment(3) %}
    
{% if not craft.retsRabbit.searches.exists(searchId) %}
    {% redirect '404' %}
{% endif %}

{% set perPage = 12 %}

{% set viewModel = craft.retsRabbit.properties.search(searchId, {
    '$top': perPage,
    '$orderby': 'ListPrice desc'
}, true) %}

{% if viewModel.hasErrors() %}
    {# An error occurred #}
{% elseif not viewModel.hasData() %}
    {# Not results returned from request #}
{% else %}
    {% set results = viewModel.data %}
    {% for listing in results %}
        {# Show listing data #}
    {% endfor %}
{% endif %}

Note: If you want to paginate your search results you will need to use our special rrPaginate tag.

Search Form

At some point your site will need to have a search form where users enter in search criteria. We've created a markup DSL for your search HTML which will allow you to create beautiful forms for your users.

Required Fields

Your search form must have the following two inputs.

  1. actionInput("rets-rabbit/properties/search)"
  2. redirectInput("search/results/{searchId}")

Note: Your redirect input must have the {searchId} term in it so that the controller endpoint which handles the form POST can redirect you to the results page with the saved search's id in the url.

We believe that the following three search types should cover the vast majority of search form use cases.

  1. Single field for a single value
  2. Single field for multiple values
  3. Multiple fields for a single value
Search Form DSL

Next, let's dive into creating a search form. In general our markup DSL follows a simple pattern:

<input name="{fieldName}(operator)" value="">.

Single Field - Single Value
<input name="StateOrProvince(eq)" value="">

This will create a query clause that looks like the following:

$filter = StateOrProvince eq {value}
Single Field - Multiple Values
{% set exteriorAmenities = ['Backyard', 'Pond', 'Garden'] %}

<label class="label">Exterior Features</label>
{% for feature in exteriorAmenities %}
    <div class="control">
        <label class="checkbox">
            <input type="checkbox" name="rr:ExteriorFeatures(contains)[]" value="{{feature}}">
            {{feature}}
        </label>
    </div>
{% endfor %}

This will create a query clause that looks like the following:

$filter = (contains(ExteriorFeatures, {value1}) or contains(ExteriorFeatures, {value2})))
Multiple Fields - Single Value
<input name="rr:StateOrProvince|City|PostalCode(contains)" class="input" placeholder="City, State, Zip..." type="text">

This will create a query clause which looks like the following:

$filter = (contains(StateOrProvince, {value}) or contains(City, {value}) or contains(PostalCode, {value}))

Note: By default, each input is treated as an independent {and} clause which are strung together to create a valid RESO query.

Example Search Form

The following example contains markup which will generate a form having the following capabilities:

  • Run a contains search against the fields: StateOrProvince, City, PostalCode
  • Run a range search (ge and/or le) against ListPrice
  • Run a range search (ge) against the fields: BathroomsFull and BedroomsTotal
  • Run a multi value contains search against: ExteriorFeatures
  • Run a multi value contains search against: InteriorFeatures
<form method="POST" action="">
    {{csrfInput()}}
    <input type="hidden" name="action" value="retsRabbit/properties/search">
    <input type="hidden" name="redirect" value="search/results/{searchId}">
    <div class="field">
        <div class="control has-icons-left">
            <input class="input" placeholder="City, State, Zip..." type="text" name="rr:StateOrProvince|City|PostalCode(contains)">
            <span class="icon is-left">
                <i class="fa fa-search"></i>
            </span>
        </div>
    </div>
    <div class="field is-horizontal">
        <div class="field-body">
            <div class="field">
                <div class="control is-expanded">
                    <div class="select is-fullwidth">
                        <select name="rr:ListPrice(ge)">
                            <option value="">Min Price</option>
                            {% for price in range(30000, 300000, 10000) %}
                                <option value="{{price}}">{{price | currency('USD', true)}}</option>
                            {% endfor %}
                        </select>
                    </div>
                </div>
            </div>
            <div class="field">
                <div class="control is-expanded">
                    <div class="select is-fullwidth">
                        <select name="rr:ListPrice(le)">
                            <option value="">Max Price</option>
                            {% for price in range(30000, 300000, 10000) %}
                                <option value="{{price}}">{{price | currency('USD', true)}}</option>
                            {% endfor %}
                        </select>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="field is-horizontal">
        <div class="field-body">
            <div class="field">
                <div class="control has-icons-left is-expanded">
                    <div class="select is-fullwidth">
                        <select name="rr:BathroomsFull(ge)">
                            <option value>Bathrooms</option>
                            {% for val in 0..7 %}
                                <option value="{{val}}">{{val}}+</option>
                            {% endfor %}
                        </select>
                    </div>
                    <span class="icon is-left">
                        <i class="fa fa-bath"></i>
                    </span>
                </div>
            </div>
            <div class="field">
                <div class="control has-icons-left is-expanded">
                    <div class="select is-fullwidth">
                        <select name="rr:BedroomsTotal(ge)">
                            <option value>Bedrooms</option>
                            {% for val in 0..7 %}
                                <option value="{{val}}">{{val}}+</option>
                            {% endfor %}
                        </select>
                    </div>
                    <span class="icon is-left">
                        <i class="fa fa-bed"></i>
                    </span>
                </div>
            </div>
        </div>
    </div>
    <div class="field is-horizontal">
        <div class="field-body">
            <div class="field">
                <label class="label">Exterior Features</label>
                {% for feature in exteriorAmenities %}
                    <div class="control">
                        <label class="checkbox">
                            <input type="checkbox" name="rr:ExteriorFeatures(contains)[]" value="{{feature}}">
                            {{feature}}
                        </label>
                    </div>
                {% endfor %}
            </div>
            <div class="field">
                <label class="label">Interior Features</label>
                {% for feature in interiorAmenities %}
                    <div class="control">
                        <label class="checkbox">
                            <input type="checkbox" name="rr:InteriorFeatures(contains)[]" value="{{feature}}">
                            {{feature}}
                        </label>
                    </div>
                {% endfor %}
            </div>
        </div>
    </div>
    <button class="button is-success" type="submit">
        <span class="icon">
            <i class="fa fa-search"></i>
        </span>
        <span>{{'Submit'|t}}</span>
    </button>
</form>

We used Bulma.io in this example, but the above markup will generate something like the following.

Search Form

Search Pagination

Because the Rets Rabbit plugin fetches data from an outside data source, it's not possible to use the native Craft pagination tag. We still believe it is very important to have the ability to paginate your results, so we created a special rrPaginate tag which works and looks like the native paginate tag in many ways.

{% rrPaginate searchCriteria as pageInfo, viewModel %}
Parameters
  • searchCriteria - An instance of SearchCriteriaModel
  • pageInfo - craft\web\twig\variables\Paginate just like with the native pagination tag
  • viewModel - A view model instance containing possible search results or errors from the API
SearchCriteria

The main difference in our rrPaginate tag compared to the native paginate tag is that it expects a SearchCriteriaModel as the first parameter. You can get an instance of a search criteria model in the following manner.

{% set criteriaModel = craft.retsRabbit.searches.criteria() %}

Once you have an instance of the criteria model, you can build your query in a fluent way by chaining method calls on that criteriaModel object above.

{#
# You must call the forId() and limit() methods for the pagination to work correctly.
#}
{% set criteria = criteriaModel
    .forId(searchId)
    .select('ListPrice', 'PublicRemarks', 'BathroomsFull', 'BedroomsTotal', 'ListingId', 'photos')
    .orderBy('ListPrice', 'desc')
    .limit(24) 
    .countBy('exact')
%}

Methods: A SearchCriteriaModel has the following methods available for building a paginated search query.

  • forId($searchId) - (required) Pass in the search id, usually from the url
  • select(...$fields) - Pass in a list of fields you specifically want from the API
  • limit($limit) - (required) How many results per page
  • orderBy($field, $dir) - Order the results
  • countBy($cacheType) - Specify the type of total results query for the API to run. Valid values are either 'exact' or 'estimated'. Uses 'estimated' by default.
Complete Example
{% set searchId = craft.app.request.getSegment(3) %}
    
{% if not craft.retsRabbit.searches.exists(searchId) %}
    {% redirect '404' %}
{% endif %}

{% set criteriaModel = craft.retsRabbit.searches.criteria() %}
{% set criteria = criteriaModel
    .forId(searchId)
    .select('ListPrice', 'StreetNumber', 'StateOrProvince', 'StreetName', 'StreetDirSuffix', 'PublicRemarks', 'BathroomsFull', 'BedroomsTotal', 'ListingId', 'photos')
    .orderBy('ListPrice', 'desc')
    .limit(24) 
    .countBy('exact')
%}

{% rrPaginate criteria as pageInfo, viewModel %}

{% if viewModel.hasErrors() %}
    <article class="message is-danger">
        <div class="message-header">
            <p>Uh oh...</p>
        </div>
        <div class="message-body">
            We could not process your request. Please try again.
        </div>
    </article>
{% elseif not viewModel.hasData() %}
    <article class="message is-warning">
        <div class="message-header">
            <p>Hmm..</p>
        </div>
        <div class="message-body">
            We could not find any results for your search. Try changing your search parameters.
        </div>
    </article>
{% else %}
    {% set listings = viewModel.data %}
    <div class="columns is-multiline">
        {% for listing in listings %}
            <div class="column is-4">
                {% include "properties/includes/_grid-item" %}
            </div>
        {% endfor %}
    </div>
    {% if pageInfo.totalPages > 1 %}
        <nav class="pagination is-centered" role="navigation" aria-label="pagination">
            {% if pageInfo.prevUrl %}
                <a class="pagination-previous" aria-label="Previous page" href="{{pageInfo.prevUrl}}">Previous</a>
            {% endif %}
            {% if pageInfo.nextUrl %}
                <a class="pagination-next" aria-label="Next page" href="{{pageInfo.nextUrl}}">Next page</a>
            {% endif %}

            <ul class="pagination-list">
                {% if pageInfo.currentPage > 2 %}
                    <li>
                        <a href="{{pageInfo.firstUrl}}" class="pagination-link" aria-label="Goto first page">First</a>
                    </li>
                    <li>
                        <span class="pagination-ellipsis">&hellip;</span>
                    </li>
                {% endif %}
                {% for page, url in pageInfo.getPrevUrls(2) %}
                    <li>
                        <a class="pagination-link" aria-label="Goto page {{page}}" href="{{ url }}">{{ page }}</a>
                    </li>
                {% endfor %}
                <li>
                    <a class="pagination-link is-current" aria-label="Page {{pageInfo.currentPage}}" aria-current="page">{{pageInfo.currentPage}}</a>
                </li>
                {% for page, url in pageInfo.getNextUrls(2) %}
                    <li>
                        <a class="pagination-link" href="{{ url }}" aria-label="Goto page {{page}}">{{ page }}</a>
                    </li>
                {% endfor %}
                {% if pageInfo.nextUrl %}
                    <li>
                        <span class="pagination-ellipsis">&hellip;</span>
                    </li>
                    <li>
                        <a href="{{pageInfo.lastUrl}}" class="pagination-link" aria-label="Goto last page">Last</a>
                    </li>
                {% endif %}
            </ul>
        </nav>
    {% endif %}
{% endif %}

We used Bulma.io in this example, but the above markup will generate something like the following.

Pagination

Other Variables

Aside from PropertiesVariable & OpenHousesVariable, there are a couple of other variables you have access to in your templates.

  • SearchesVariable - craft.retsRabbit.searches

SearchesVariable

This template variable has the following methods:

  1. exists

bool exists(int $id)

This method checks if a given search id exists. This method is useful for checking if a search exists before trying to execute it which will provide more predictable error handling.