Utopian.info - Autocompletion and searching

in #utopian-io7 years ago (edited)

image.png

Note: the changes in this contribution won't be live on https://utopian.info as I am completely reworking the application structure and it would break the website if I added it. If you want to test it out locally then make sure to clone the develop branch and not the master branch: https://github.com/amosbastian/utopian/tree/develop (instructions are in the README).

What's new?

The ability to search for stuff on Utopian.info has long been missing, so I added it! It uses typeahead.js for autocompletion, flask_wtf and wtforms for the forms and PyMongo and Flask for the actual searching and showing the results themselves.

Some of you may remember seeing search forms for each section on the home page in my last contribution. The basic functionality of these is now working, and I have also added another form to the header which allows you to search anywhere on Utopian.info (see image above). I will go over the changes below and explain how they were implemented!

Autocompletion

On the home page

To implement the autocompletion I used typeahead.js which made it very easy. Most of the code I used is exactly the same as the examples they give here. The only problem I faced was getting the data that is actually used for the autocompletion. To do this I added a context processor in the application factory that injects the data into the base.html template so it can be used by typeahead.js.

@app.context_processor
def inject_autocompletion():
    moderators = DB.moderators
    posts = DB.posts
    manager_list = [moderator["account"] for moderator in
                    moderators.find({"supermoderator": True})]
    moderator_list = [moderator["account"] for moderator in
                        moderators.find({"supermoderator": False})]
    contributor_list = posts.find().distinct("author")
    project_list = posts.find().distinct("repository.full_name")
    return dict(
        manager_list=manager_list,
        moderator_list=moderator_list,
        contributor_list=contributor_list,
        project_list=project_list,
    )

These lists each contain all the names of all contributors or projects on Utopian for example, and since they are injected into the base.html template it means they are available for use everywhere. To make them available to typeahead.js I simply added the following to base.html (there is probably a better way)

<script>
  var managers = {{manager_list|tojson|safe}};
  var moderators = {{moderator_list|tojson|safe}};
  var contributors = {{contributor_list|tojson|safe}};
  var projects = {{project_list|tojson|safe}};
</script>

which is then used in js/typeahead.js like so (just like the example)

$('#projects .typeahead').typeahead({
  hint: true,
  highlight: true,
  minLength: 1
},
{
  name: 'project',
  source: substringMatcher(projects)
});

Of course this has also been styled, which results in the following when searching for contributors on the home page for example

autocompletion_example.gif

In the header

Of course you should be able to search everywhere on Utopian.info so I added a search form to the header as well. I really like the look of GitHub, so I tried making it similar to theirs. Anyway, since this allows you to search anywhere I made it so it autocompletes for managers, moderators, contributors and projects using another example they give that uses multiple datasets. As you can see in the GIF below it works really well

header_autocompletion.gif

FlaskForm

For each search form I have created a class in forms.py using flask_wtf. To add this I followed this tutorial where everything is described in great detail. An example of one of the classes can be seen below

class ContributorForm(FlaskForm):
    """
    Form for handling the contributor search on the home page.
    """
    contributor = StringField(
        "Username",
        validators=[DataRequired()],
        render_kw={
            "placeholder": "Contributor",
            "id": "contributor"
        }
    )

    def __init__(self, *args, **kwargs):
        if "formdata" not in kwargs:
            kwargs["formdata"] = request.args
        if "csrf_enabled" not in kwargs:
            kwargs["csrf_enabled"] = False
        super(ContributorForm, self).__init__(*args, **kwargs)

Once a form like this has been passed to the template you can simply render it using the following code

<form action="{{ url_for('search.index') }}" method="get" class="header-search">
  <div class="header-search__group" id="search">
    {{ search_form.q(class="header-search__input typeahead") }}
    <input type="submit" style="position: absolute; left: -9999px">
  </div>
</form>

After submitting the form the data in it is sent to a Python script where the data can be used to return the search results.

Getting the search results

Of course once a user searches for something you should also return the results. When I first started implementing this I was thinking of using text indexes since MongoDB provides them to support text search queries on string content. In the end I implemented this with PyMongo's find() method by simply matching the given data with a regex.

def contributor_search(data):
    """
    Returns a list of distinct contributors that match the given data.
    """
    posts = DB.posts
    search_result = posts.find({"author": {"$regex": data}})
    return list(search_result.distinct("author"))


@BP.route("/contributor", methods=["GET"])
def contributor():
    """
    Handles the contributor form on the home page.
    """
    contributor_form = ContributorForm()
    search_result = contributor_search(contributor_form.contributor.data)
    if len(search_result) == 1:
        # TODO: Redirect to contributor's page
        return "{}'s page...".format(search_result[0])
    return jsonify(search_result)

Each form on the home page has its own route in search.py which then in turn uses another function to get a unique list of the names of e.g. contributors as seen in the code above. Currently it is very basic and checks if the query only matches one contributor or more than one. If only one contributor is returned then (to be implemented) it will redirect the user to that contributor's page, otherwise it will show a list of the contributors that matched the given data.

Since the form in the header allows the user to find managers, moderators, contributors and projects it simply uses the functions that were implemented for the specific forms to retrieve four lists containing the results. The results are then shown on a page, as you can see in the GIF below

search_result.gif

Pull request

What are my plans?

The next feature I want to complete is fleshing out the search results page. I would like it to be something like GitHub's one where you would be able to choose between managers, moderators, contributors and projects, all with sorting and pagination of course! Unfortunately I have a pretty busy period coming up at university, so I will have to see how much work I can get done.

Contributing

If you want to contribute, then please read CONTRIBUTING.md for more information.



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Hey @amosbastian

We're already looking forward to your next contribution!

Decentralised Rewards

Share your expertise and knowledge by rating contributions made by others on Utopian.io to help us reward the best contributions together.

Utopian Witness!

Vote for Utopian Witness! We are made of developers, system administrators, entrepreneurs, artists, content creators, thinkers. We embrace every nationality, mindset and belief.

Want to chat? Join us on Discord https://discord.me/utopian-io

Nice work @amosbastian,
Waiting for this update on utopian.info

I'm also looking forward to when everything is ready!

Thank you for the contribution. It has been reviewed.

  • Great use of animated gifs and code highlight.
  • A few more comments in the code would be great.

Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.

[utopian-moderator]

Keep the good work up @amosbastian

Great work @Amosbastian and great reportage. I like the way you lined up everything. Kudos and more grease to your elbows.

Thanks for the kind words!

I haven't coded in days and I really really feel like this is the project. Python? Utopian? Sign me up.

Let me know if you have any ideas that you're too lazy to implement. I seriously want to get on this.


Need help? Write a ticket on https://support.utopian.io.
Chat with us on Discord.

[utopian-moderator]