Including an extra button in a crispy form

I’m a big fan of using crispy forms with Django. In fact, since doing the Django girls tutorial, I don’t think I’ve made a form manually.

And, until now, I haven’t been super critical of aesthetics. Now, however, I’m updating the look of the worksheet generator and moved the login to a modal. Doing that, I thought… “Hmm, wouldn’t it be nice if the ‘reset password’ button was in a button group with the login button?

It turned out to be a bit of a challenge for me, and I couldn’t find anything on it specifically, so here’s what I did:

loginModal
The goal — Doesn’t that look nice?
oldLogin
What I had before. Too much screen real estate.

Functionally, it’s not a big deal, except that I’m using button groups more and more in the site in general, and why not use one here?

However, the old way I was rendering the form wouldn’t work. The crispy FormHelper object rendered the form so completely that I couldn’t get at the login button, much less wrap it together in in a button group.

Of course it was possible, but the solution turned out being telling crispy forms to let me create my own form tag:

# forms.py

class LoginForm(forms.Form):
    username = forms.CharField(label='Username:', max_length=100)
    password = forms.CharField(label='Password:', widget=forms.PasswordInput(), max_length=100)

    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_tag = False

The self.helper.form_tag = False bit turns off the form field.

Then, in the template rendering the form, I had to make some changes. To be honest, I should say that I took the easy way by having crispy forms render the form tag, and then copied that.

template
WordPress didn’t want to display this, so this seemed the easiest thing to do.

Initially, I did this without adding type=”button” to the second button in the group and, for reasons I don’t understand, both buttons functioned at a submit button. Obviously, that wasn’t ideal, and adding the extra type=”button” made it work.

It’s worth pointing out that I never (or, not yet) removed the old …/login.html page that rendered the form before. Nothing links to it, so I doubt anyone will ever hit it. But, if they did, the form would be broken because I didn’t update that template to include a form tag.

Advertisements

It grows!

Just a simple idea…

So, I invited some of the people who tried dynamic-efl.com (my worksheet making site) a while ago to return to the site and see if things appeared more streamlined, clearer. And what was the response?

“Toby, I tried to login, but I’ve forgotten my password. Doesn’t your site have one of those password-reset functions?”

Of course it doesn’t. Grr.

No problem, it should be pretty straight-forward to code and it needs to get done at some point, I suppose.

It still isn’t done.

Save the email addresses

It turns out that the way I had been creating users, I managed to accidentally forget to save their email addresses. Oops. So, even if the “email me a link to create a new password” function existed, it wouldn’t help.

I’ve fixed that.

Then, once I was working with the email addresses, it seemed logical to add them to the mailchimp list I’d been thinking about starting.

I got that done.

Of course, to avoid adding bad email addresses to the mailchimp list and getting flagged, it seemed logical to first add the addresses after they’ve been verified. But that required me to code up an email-verification system. No biggie, but that meant learning how to send email through django.

I got that done. (Interestingly, I can send it through gmail, which I find convenient.)

Since that was done, it seemed as good a time as any to add an ‘account_locked‘ flag to the Teacher model, which would throw up a message on several of the relevant pages saying “your account is locked…”

I got that done.

accountlocked

Then, it seemed ridiculous to have a ‘your account is locked’ message if the account wasn’t, actually, functionally locked. Once I dug out where exactly the downloads happened, it should be pretty simple to have it first check if the account is locked or not.

I got that done.

There still isn’t a password reset

It’s next on my list of things to do, but this sort of experience is pretty standard for me in my own coding. I think everyone imagines that ‘real coders,’ have a plan and sit down and build something logical from start to finish.

(Though there is comic evidence to suggest that isn’t true. I got this via “Coding explained in 25 profound comics,” which is worth a visit on its own.)

coding

So, perhaps this is just par the course for the coding experience. Either way, it’s part of why my next project will be to write a drop-in user management module for future django projects.

I’m enjoying this and learning to appreciate a lot about website administration. But I would not be upset if I only had to learn all this once.

If you’ll excuse me now…

…I’m off to write a password reset function.

Mailchimp’s Subscriber Hash

I learned something today. In sitting down to finish up my mailchimp functions, I wanted to create a mailchimp_remove_teacher_from_list(listname) function and a mailchimp_blanket_unsubscibe_teacher() function. I thought that, after my previous success, it would go quickly.

I was wrong.

Over here, in the docs, there’s reference to how to delete list members, which is what I basically want to do:

client.lists.members.delete(list_id=”, subscriber_hash=”)

I knew what a list_id was, but wasn’t sure what the subscriber_hash was. After a bit of Googleing (I thought I’d have to ask mailchimp for the hash) I found this page and was able to piece it together:

In previous versions of the API, we exposed internal database IDs eid and leid for emails and list/email combinations. In API 3.0, we no longer use or expose either of these IDs. Instead, we identify your subscribers by the MD5 hash of the lowercase version of their email address so you can easily predict the API URL of a subscriber’s data.

For example, to get the MD5 hash of the email address Urist.McVankab@freddiesjokes.com, first convert the address to its lowercase version: urist.mcvankab@freddiesjokes.com. The MD5 hash of urist.mcvankab@freddiesjokes.com is 62eeb292278cc15f5817cb78f7790b08.

In short, the subscriber_hash argument is something you produce at your end and use to look a subscriber up. There’s something elegant about it — you have no business accessing the subscriber if you don’t know the email address, already — but I needed some time to get it done.

Here’s how I did it:

import hashlib # A standard library that does hashes

# The following is in .lower() case because mailchimp forms
# hashes from lowercase strings.
# The .encode() method tagged on the end encodes it as a byte literal
email = request.user.email.lower().encode(encoding='utf-8)

# This uses the hashlib library to make the hash. The .hexdigest()
# seems to be about equivalent to str() [forgive me internet!]
hash = hashlib.md5(email).hexdigest()

client.lists.members.delete(list_id=list_id, subscriber_hash=hash)

This is not a complete solution. It basically tags onto my last post. At some point, I want to compend everything I do into something more universally applicable. When I do, I have no plans to keep it secret.

Adding Mailchimp actions to a Django model

As I get ready to try a second time to attract users, I decided to use MailChimp mailing lists to keep in touch with these users, as well as setting up some ‘tutorial’ emails at regular intervals. (I’m imagining something like “You’ve been using dynamic-efl.com for a week now, did you know you can do this to get more out of it?”)

To do this, I’d have to have code in Django that automatically signed members up. That means that this is my first attempt to work with an API, even by way of a library.

I decided to go with the mailchimp3 library and installed it in my virtualenv according to the instructions and set up some variables in my settings.py file.

# Mailchimp stuff
# Authentication
MAILCHIMP_LOGIN = 'login'
MAILCHIMP_KEY = '===>SECRET<==='

# The lists
MAILCHIMP_LISTS = {'welcome': 'listID',
                   'germany': 'listID',
                   'beta-testers': 'listID'}<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

The idea here is that I’ll be able to iterate over the keys in the lists, if I ever need to check that the user is in all the lists (for example, removing a user should include removing him from all lists).

To that end, I added a function at the beginning of my models.py file. (This probably isn’t good form).

def mailchimp_get():
    """ Returns a logged-in mailchimp client """
    return MailChimp(settings.MAILCHIMP_LOGIN, settings.MAILCHIMP_KEY)<span 				data-mce-type="bookmark" 				id="mce_SELREST_start" 				data-mce-style="overflow:hidden;line-height:0" 				style="overflow:hidden;line-height:0" 			></span>

It seemed like the easiest way to only write the login once.

Finally, I had to create a few methods in the Model in question (in my case, this is my teacher model, as my users are called ‘Teachers’ in this bit.

def mailchimp_add_teacher_to_list(self, listname):
    """ Takes a listname, checks that the teacher is not a member of the list, then adds it. """
    client = mailchimp_get()
    listid = settings.MAILCHIMP_LISTS[listname]
    if not self.mailchimp_teacher_in_list(listname):
        data = {
            'email_address': self.user.email,
            'status': 'subscribed',
            'merge_fields': {
                'FNAME': self.user.first_name
            }
        }
        client.lists.members.create(listid, data)

def mailchimp_lists_get(self):
    """ TEMPORARY - Returns a list of all mailchimp lists """
    client = mailchimp_get()
    return client.lists.all(get_all=True, fields="lists.name,lists.id")

def mailchimp_teacher_in_list(self, listname):
    """ Returns a bool indicating if this teacher's email address is in the list """
    email = self.user.email
    client = mailchimp_get()
    results = client.lists.members.all(settings.MAILCHIMP_LISTS[listname], get_all=True, fields="members.email_address")
    members = results['members']
    addresses = []
    for m in members:
        addresses.append(m['email_address'])

    return email in addresses

I think the names should be pretty self-explanatory. The mailchimp_lists_get() method was just an experiment based on following the instructions in the documents.

Lastly, I created a view and a URL just to test these functions by printing to the console and looking at the (currently empty) lists in mailchimp.

Summary

It was my first time sitting down and thinking critically about what I wanted and how to do it and I have to say that mailchimp and the library basically made it painless. I have other project ideas that will require more API integration, and it’s made me hopeful.

Updating the Interface

I have been busy, and frustrated, as I work on the worksheet generation app. And, unfortunately, I haven’t been adding new functionality or improving the existing functionality. At least, I haven’t been improving it from the point of view of how the worksheets are made. I’ve been working on how people make the worksheets.

This is what the app interface used to look like:

old-formattingIt really isn’t bad, it’s just very basic. I knew I’d change it at some point — I use this site more than anyone else, and I have an idea how I want it to look — but I wanted to focus on features.

Then, recently, I invited friends and colleagues to try the website out. And nobody seemed to understand what it did. I told them (in emails, in person) and they loved the idea, but the interface that seems obvious to me… well, it obviously isn’t.

I had an idea: I’ll make a sort of tour you take, before you can sign up, that will explain the functionality with screen-grabs. 

But, there are so many things on my plate, I know the chances of me making the same tour twice are not great. At least, not in the same year. So, logically, the interface should be changed.

CSS is hard!

The thing about CSS is this: you can  download templates (like I did for the landing page, from a great resource called Start Bootstrap), and I figured I could easily adapt the rest of my page to match it.

Wrong!

To be fair, I haven’t yet come across a CSS problem that, once I figured out how to articulate it correctly, didn’t have a clear answer somewhere. (Also, I love the resources provided by W3Schools, but hate their search functionality. Are they trying to make money there?) But, every step felt like an uphill battle.

Eventually, I got to a point where I was starting to be satisfied with the result.

Hosting static files on django is poorly explained!

I’m starting a list of things that were hard for me to learn about django, to eventually explain when I feel like I understand them. One thing would be the deployment to linode (remember that?) and another would be hosting static files.

After a lot of googling and basically blindly trying things, I’ve stumbled into a configuration that worked. But, for a while, it only worked on the linode site and not locally and I was too frustrated to change it. So, every change I made had to be pushed to the site to be tested, which was also frustrating.

Then, I got around to working out my development file hosting and was able to test changes more quickly (and without feeling like I’d have ‘broken’ the site just then when a friend would try to use it), but when I pushed my changes to the linode server, I realized that I’d broken hosting there!

Now, things seem to be working both locally and on the server, and I’m going to not play around with it. But, I have other projects cooking, and I’ll need to wrap my head around this.

So, what does it look like now?

Obviously, after all that drama, you’d expect something amazing, right? Well, it turns out that I can fix individual problems, but I don’t really have much of a vision of an overall design. So, while the layout is basically the same, the new formatting is something I’d rather spend my time looking at:

new-formatting

It has a title bar that remains fixed as you scroll, and functions are presented as buttons (because they seem to suggest action) and, rather than telling you “there is enough vocabulary to make a memory activity,” it simply doesn’t show you the option unless there is sufficient memory.

I can live with this for the rest of the academic year, I think. Now, I just have to clean up all the pages and make a tour, as well as finishing up my ‘minimum functionality…’

Preparing for Clowns, or what I learned from a Twitter-fight

The CodeNewbie chat last night was about dealing with bullies, and, since that dovetailed so well with what I’m working on right now, I thought I’d try to be a bit more active in blogging about what I’m doing.

You see, I think my app is 90% finished, but I’m taking a break from features to get ready for the public. Which, really, means, to get ready for the jerks. You see, the worksheet generation app isn’t a social app. The user’s interaction with it is basically all centered on the user and their own groups and classroom resources for their groups.

But, contributions are shared. That is to say, if you need a translation for the word ‘web app’ for one of your groups and there isn’t one in the system, the system just asks you for one. But then, the next guy profits by the translation you added. And you’re profiting from the resources added by others.

The Twitter Fight

But then, I got in a bit of a fight on Twitter. I’ll post sometime on the idea of ‘defending the ancestors,’ and why I don’t like the term ‘the ancestors’ being used as a dog-whistle for racism (racist dogs!) but that was what it was about. And it wasn’t nice.

While I’m a middle-child and can handle Internet strangers being mean to me, I realized that the guy I was fighting with had the advantage: his Twitter handle was only for his trolling. My Twitter handle connected to all my life, and he could see that, use it in a fight. And, because I tend to fear the worst, he could start being a jerk in other parts of my life.

I realized that, when I start posting about the worksheet generation app by name, with links, anybody who felt like they didn’t like me (and there have been a lot of those people in my life) could create a free account, and just throw a wrench in the works by adding terrible resources.

Imagine if a legitimate user — maybe even one paying for the service — wanted that aforementioned translation of ‘web app’ and got something like ‘where we see the naked photos of your mom.’ Even worse, what if that person — like I often do — didn’t proofread that worksheet before handing it out to students. (At some point, worksheets will be emailed with a mouse-click.) What a nightmare.

Worse than paranoia

I talked myself back down from “Twitter people will be mean.” After all, the solution could easily be ‘just charge everyone.’ Who’d pay me money for the privilege of trolling me?

Then I realized I shouldn’t worry about Twitter trolls making accounts and being jerks. I should worry about people just like me thinking that they’re being funny. Or, even worse, thinking that the resource they’re making is appropriate for their students. After all, we teach adults, whose to say that we can’t include “your mom” jokes?

My real  nightmare is someone either testing out the system, playing around, and putting random crap in… Just to see if it really appears in a worksheet.

Of course there’s a solution

I can deal with it. Of course I can. There’s not a problem without a solution.

My interim solution is, for now, giving users the opportunity to explicitly say “this is part of an inside joke with this group.” You can easily add resources that will only be used for a single group. Easy.

The other thing is pretty simple: all resources are immediately available to the person who creates it. Everything else has to be approved, first. As users are added, it will be possible to say “the resources added by user_x will be automatically available,” but, until then, it means that I’ve invested a lot of time already (all morning today was spent creating the interface and the backend that will enable this level of moderation) into the project of investing a lot of extra time in this project.

And that kinda stinks. It makes me like people, as a group, just a little bit less.