Self Service Photo Downloads

  • By Daniel Hazelbaker

At RX2018, Jeremy briefly showed off a self-service photo download system we used earlier this year on Mother's Day. Since his time was limited, we didn't go into the various details for how to configure this system... As far as Jeremy knows, I spent weeks and weeks building custom code to pull this off. However, you get the inside scoop and are going to learn how quick and simple this was to implement. But please, don't tell Jeremy how easy this was otherwise he'll realize he doesn't need a developer on staff.

Okay so seriously, this is one of those "looks really really cool but was super easy to implement". There are two pieces. The first is a Workflow that staff uses to upload the photos. The second is a front-end Dynamic Data block that allows users to find their pictures by the code number.

Workflow Setup

So we are going use a Workflow to allow staff to upload pictures. We are going to call this workflow Photo Card Upload and then setup two attributes.

    • Name = Card Number
    • Key = CardNumber
    • Field Type = Text
    • Name = Image
    • Key = Image
    • Field Type = Image

I highly recommend you pick a file type for the Image attribute that does not store the file data in your database. Each image could easily be 4MB or more and you don't want all that data stuffed into your database. Use a cloud storage provider or at the very least configure the file type to store the data on the file system. Also, create a new file type for this so that you can easily identify these files later for cleanup.

Next we are going to configure two activities in the workflow. The first is the Start activity and the second will be called Redirect. Go ahead and create both of those before adding any actions.

For the Start activity, add a new Action named Upload Form and make it of type User Entry Form. There will be no notification e-mail. We are going to mark both Form Fields as Visible, Editable and Required. We will have the standard Save command button, but we are going to add one additional command button: Save and Add. So add an additional command button, labeled Save and Add, button type is Secondary and then select the Redirect activity for the activate activity option. No response text is needed.

The next action in our Start activity will be a simple Workflow Persist action.

Moving on to the Redirect activity, we are going to have a single action which is the Redirect to Page action. This will redirect the user back to the workflow entry page to begin uploading another picture. So the Url value will be /WorkflowEntry/{{ Workflow.WorkflowTypeId }} and the Processing Options should be set to Always continue.

That completes the setup of the Workflow! Pictured below is the workflow in it's entirety.


Dynamic Data Block

Okay, so that allows staff to upload photos. But we need a way for end-users to get their photos. We are going to do this with a Dynamic Data block. So start by creating yourself a new page on your public site. Edit the page settings and set the route field to MyPhotos/{CardNumber},MyPhotos.

Now add a new Dynamic Data block and edit it's settings. There are only 3 things you need to set. The first is easy, set the Parameters to CardNumber=0. This will define the @CardNumber we will use later in the SQL and give it a default value so that it doesn't cause any errors (and also will never find results if not provided).

Next we need to set the SQL Query to use:

    , AVCard.[Value] AS [CardNumber]
    , AVImage.[Value] AS [Image]
FROM [Workflow] AS W
INNER JOIN [Attribute] AS ACard ON ACard.[EntityTypeQualifierColumn] = 'WorkflowTypeId' AND ACard.[EntityTypeQualifierValue] = W.[WorkflowTypeId] AND ACard.[Key] = 'CardNumber'
INNER JOIN [AttributeValue] AS AVCard ON AVCard.[AttributeId] = ACard.[Id] AND AVCard.[EntityId] = W.[Id]
INNER JOIN [Attribute] AS AImage ON AImage.[EntityTypeQualifierColumn] = 'WorkflowTypeId' AND AImage.[EntityTypeQualifierValue] = W.[WorkflowTypeId] AND AImage.[Key] = 'Image'
INNER JOIN [AttributeValue] AS AVImage ON AVImage.[AttributeId] = AImage.[Id] AND AVImage.[EntityId] = W.[Id]
INNER JOIN [BinaryFile] AS BF ON BF.[Guid] = AVImage.[Value]
WHERE W.[WorkflowTypeId] = 92 AND AVCard.[Value] = @CardNumber

In the SQL above, on the last line you will need to replace 92 with the Workflow Type Id of the workflow you just created. And finally, we are going to use a custom Formatted Template:

    .photo img {
        border: 6px solid lightgray;
        border-radius: 3px;
    .photo img:hover {
        border-color: darkgray;

<div class="photos margin-b-lg">
    {% assign size = rows | Size %}
    {% if size == 0 %}
        {% if PageParameter.CardNumber %}
            <h3>We couldn't find any photos for that code. You can try entering another code <a href="/MyPhotos">here</a>.
        {% else %}
            <h3 class="text-center margin-b-md";>Looking for your photos? Enter your code below.</h3>
            <div class="row margin-b-lg">
                <div class="col-md-4 col-md-push-4">
                    <div class="input-group">
                        <input type="text" class="form-control input-lg js-photo-code" placeholder="Code">
                        <a href="#" class="input-group-addon btn btn-primary js-photo-code-search" style="font-size: 18px;">Search</a>
                $(document).ready(function () {
                    $('.js-photo-code-search').on('click', function (e) {
                        if ($('.js-photo-code').val() != '') {
                            window.location = '/MyPhotos/' + $('.js-photo-code').val();
                    $('.js-photo-code').on('keypress', function (e) {
                        if (e.which == 13) {

                            if ($('.js-photo-code').val() != '') {
                                window.location = '/MyPhotos/' + $('.js-photo-code').val();
        {% endif %}
    {% else %}
        <h2>My Photos</h2>

        <div class="row">
            {% for row in rows %}
                <div class="col-md-4">
                    <div class="photo">
                        <a target="_blank" href="/GetImage.ashx?Guid={{ row.Image }}">
                            <img src="/GetImage.ashx?Guid={{ row.Image }}&maxheight=420" class="img-responsive">
                {% assign mod = forloop.index0 | Modulo:3 %}
                {% if mod == 0 and forloop.first == false %}{{ '</div><div class="row">' }}{% endif %}
            {% endfor %}
    {% endif %}

Okay, so here is what all this lava is doing. First, the <style> section is just making the pictures look a little nicer by adding a small rounded border on each picture.

Next we check how many rows we got back from our query. If we did not get any rows back then it means one of two things. Either the user didn't enter a card number (i.e. just the /MyPhotos route) or they entered a card number (i.e. the /MyPhotos/XYZ route) but no photos matched that number. So we then next check if they entered a card number and if they did simply display a message letting them know we couldn't find any photos.

Otherwise, if we got no results but they didn't provide a card number yet, then we throw some HTML and JavaScript on the page to allow the user to enter a code. The HTML section simply puts a short message on screen asking them to enter the code number and provides an input field and search button. The JavaScript allows the user to either click the Search button or press the Enter key inside the text field. Either one will cause the page to redirect to the full /MyPhotos/XYZ route which will reload the page and perform the search on the code entered.

Now, we are down to the else section where we found one or more results from our SQL query. This is a pretty simple section. All we are doing is looping through the images and displaying them at thumbnail size inside a hyperlink so the user can click on the thumbnail to view a full-sized image or download the image.

Finally, remember to completely reload the page after editing the Dynamic Data block so that all the JavaScript registers correctly since we are adding it dynamically.

That is it, you should now have a functional system where you can pre-print cards to hand out to people with the codes as well as full URL: One thing to remember, the codes will be case insensitive and some characters look the same. So we recommend you generate your codes in all upper case and exclude a number of characters that are similar: 1, i, l, 0, o

Note: Currently, these workflows will stick around forever which means you cannot re-use codes. In Rock 8.0 there will be a new Workflow configuration option called Completed Workflow Retention Period that will let you specify how long ot keep completed workflows before they are deleted from the system. Once you upgrade to Rock 8.0 you can set this to, for example, 90 days and those workflows will be deleted after 90 days. Just be aware that it will not delete the uploaded files, just the workflows.

Shepherd Church

Prolific coding monkey and avid Overwatch player. If there is a "right way" to do something you can be sure Daniel's approach will be a "near miss".