Tuesday, April 26, 2011

Hiring

PRACA
Programista Python / django

Co robimy? Ciekawe projekty związane z aplikacjami internetowymi, które wymagają nieszablonowego podejścia.

Kogo szukamy? Studentów, którzy chcą rozwijać swoje umiejętności w technologiach internetowych (python, django, html 4 i 5, css 3, JavaScript, jQuery, SQL, mysql, postgres).

Co oferujemy? Dogodne godziny pracy dla studiowania, miłą atmosferę i duży nacisk na rozwój przez naukę od doświadczonego zespołu.

Miejsce: Kraków
Kontakt: python@dorosz.com 

Saturday, April 2, 2011

Flat but clever menuing for django

While creating a website you may need a menu at some point. Maybe even more than one. Django does not have any menuing system, so after searching for ready menu app I still was not satisfied of what I found. All of stuff that I've found were neither complicated or had some strange architecture (e. g. based on ORM).

So I wrote it by myself with such assumptions:
  1. I do really need only flat menus - but still I can use several flat menus to emulate nested menus.
  2. Menuing should work in every view without putting any arbitrary @decoration or python code to view function code.
  3. Menu should be able to draw items in the way that template designer wants it to have, no hardcoding of CSS classes for "selected" items.
  4. No database hits for getting menu - this is obvious, menu does not change at all, why to hit database for it?
  5. Finally the clever thing - the fact that any item of menu is selected should be evaluated from current URL of view request.
So the first thing to do is to create reusable django app. 

~$ ./manage.py createapp menus

Definition of the template tag is the only thing that is important in the menus app:
./menus/
./menus/templatetags/
./menus/templatetags/__init__.py
./menus/templatetags/menus.py

In menus.py put following code:
from django.template import Library, Node, TemplateSyntaxError
import re

register = Library()
def match_path(match, path):
 if re.search(match, path):
  return 'selected'
 else:
  return ''
 
class MenusNode(Node):
 def __init__(self, model, varname):
  self.varname = varname
  self.model = model
  self.project_menus = __import__('menu')
 
 def render(self, context):
  menu_list = getattr(self.project_menus, self.model)()
  menu_list = map(lambda x: {'name': x['name'], 'url':x['url'], 'selected': match_path(x['match'], context['request'].path)} , menu_list)
  context[self.varname] =  menu_list
  return ''

def get_menu(parser, token):
 bits = token.contents.split()
 if len(bits) != 4 or bits[2] != 'as' :
  raise TemplateSyntaxError, "bad syntax: get_menu menu_name as menu_variable for url"
 return MenusNode(bits[1],  bits[3])
get_menu = register.tag(get_menu)

Now add new app to your settings.py to INSTALLED_APPS.

Create in your project root dir (the same where you keep settings.py) file called menu.py.

menu.py
from django.core.urlresolvers import reverse
def menu_main():
 return [ 
   { 'name' : 'Home',
    'url' : '/',
    'match' : r'^/$',
    },

   { 'name' : 'Pricing',
    'url' : '/pricing/',
    'match' : r'^/pricing/',

    },
   { 'name' : 'Blog',
    'url' : '/blog/',
    'match' : r'^/blog/',

    },
   { 'name' : 'Contact',
    'url' : '/contact/',
    'match' : r'^/contact/',

    },
   { 'name' : 'Sign-in',
    'url' : reverse('accounts.views.signin'),
    'match' : r'^' + reverse('django.contrib.auth.views.login'),

    },
   { 'name' : 'Your account',
    'url' : reverse('accounts.views.accont'),
    'match' : r'^/logged/',

    },
 ]



That's all folks. Now you can use your menu in the template. If this is main menu, it is wise to use it in top level template that you will extend from.

{% load menus %}
<html>
...
<body>
...
<div id="menu">
<ul>
 {% get_menu menu_main as menu_main %}
 {% for  menu_item in menu_main %}
  <li><a href="{{menu_item.url}}" class="{{menu_item.selected}}">{{menu_item.name}}</a></li>
 {% endfor %}
</ul>
</div>
...
</body>   
</html>

As you can see, your menu will be generated from the data structure that is returned after calling appropriate menu name from menu.py. This allows you to even create dynamic menus if needed.

The format of structure is a list of tuples. Each tuple contain three values:
  1. Display name for menu.
  2. URL address that the menu item should direct to after clicking.
  3. Regex to match against current URL in order to determine if particular item should be marked as "selected". This simple approach gives you many possibilites to activate menu items on different set of URLs by making alternations in regex. 

Avoiding hardcoding
  1. If you don't want to hardcode regular expressions into menu.py, just change them to r'^' + reverse(...). This will create a regular expression from the link you are pointing to (what is usually good idea).
  2. If you don't want to hardcode class="selected" just change it to class="{% if menu_item.selected %}my_fancy_selected_class {% endif %}".


Nested menu

In my case nested menu is when you want to have a main menu at the same time with additional menu (e. g. for logged user). One of items from main menu may be selected as an ancestor item for logged "sub" menu.

You can easily create the second menu, only remember to make URLs in prefix style. For example:

http://mydomain.tld/pricing
http://mydomain.tld/blog
http://mydomain.tld/contact

http://mydomain.tld/logged/view1
http://mydomain.tld/logged/view2
http://mydomain.tld/logged/view2/add/new/item
http://mydomain.tld/logged/view3
http://mydomain.tld/logged/view3/some/other/action

When you try to display both menus the item "Your Account" in menu_main will still be active for all "logged" pages because regex will still match for this urls (there is no $ sign at the end of regex). And you can have your nested menu_logged with some different items to display.
def menu_logged():
 return [ 
   { 'name' : 'Action 1',
    'url' : reverse('myapp.logged.view1'),
    'match' : r'^' + reverse('myapp.logged.view1'),
    },

   { 'name' : 'Action 2',
    'url' : reverse('myapp.logged.view2'),
    'match' : r'^' + reverse('myapp.logged.view2'),

    },
   { 'name' : 'Action 3',
    'url' : reverse('myapp.logged.view3'),
    'match' : r'^' + reverse('myapp.logged.view3'),

    },

 ]



Notice that every other action made from view2 and view3 will also select Action 2/Action 3 in menu_logged. That's because we use URL prefixing for organizing the URL namespace of django project. Even if that additional actions does not have their menu items - with url prefixing you can assign them to the particular menu item.

Functional python programming in April Fools

This is not djangish stuff but still tooks me a while of hacking.

I wanted to make an April Fools Day joke on my dear students and mess up their names on student records.

All I had need to do is to export Google Spreadsheet as CSV, read first column with "surname firstname" format. Then split surnames from firstnames, sort the firstnames list and join them again.
Aaby Joe
Daby Ann
Zaby Ian
to
Aaby Ann
Daby Ian
Zaby Joe

So I make a simple python script:

import csv
reader = csv.reader(open('notes.csv', 'rb'), delimiter=',', quotechar='"')
i = 0
surnames=[]
firstnames=[]
for row in reader:
 if i > 2:
  surnames.append( row[0].decode('utf-8').split(' ')[0] )
  firstnames.append( row[0].decode('utf-8').split(' ')[1]  )
 i+=1
firstnames.sort()
newnames = map(lambda x: x[0] + ' ' +x[1], zip(surnames, firstnames))
print newnames

I looked at this and thought - what a long python code. This is so simple task, shouldn't be written in so sparse way. Also omitting the two first lines looks awful. This should be done with slicing.

After while of hacking I've end up with this:

import csv
print  map(lambda x: "%s %s" % (x[0], x[1]), reduce(lambda x, y: zip(x, sorted(y)), zip(* map(lambda x : x.split(' '), [row[0] for row in csv.reader(open('a.csv'))][3:]))))

But this became a typical one-liner that is completely unreadable after one week. I felt that I need to make some indentation to this code anyway to make it readable:

import csv
print  map ( lambda x: 
            "%s %s" % (x[0] , x[1]), 
          reduce ( 
            lambda x,y: 
               zip(x, sorted(y)), 
            zip (* map(
                  lambda x: 
                     x.split(' '),  
                  [row[0] for row in csv.reader(open('notes.csv'))][3:])
               ) 
         ) 
      )

So functional programming is fun, but the problem is you need to read code inside-out, not bottom-down and that is sometimes difficult. I am wondering if there will be someday a programming language that will support functional programing with bottom-down oriented syntax.

Polymorphism in django models

Django models are very powerful ORM, but they do not play nice with any kind of polymorphism out of the box. The problem cames when you want to add a model that have some generic properties set, but it can differ in implementations of some methods (because of theirs type/specialization).

Lets consider follwoing example:
class Animal(models.Model):
 name = models.CharField(max_length=15)
 
 def __unicode__(self):
  return self.name
 def make_sound(self):
  print "Some sound"

I would like to store different types of Animals here: a dog, a cat, a cow. All I am interested with is theirs name, so I don't want to make a separate models for them. Also I would like to make querysets regardless of a kind of an animal - just from all of the animals I have.

>>> cat = Animal(name="Cattie").save()
>>> dog = Animal(name="Doggie").save()
>>> cow = Animal(name="Cowwie").save()
>>> Animal.objects.all()
[<Animal: Cattie>, <Animal: Doggie>, <Animal: Cowwie>]
>>> for a in Animal.objects.all():
...    a.make_sound()
... 
Some sound
Some sound
Some sound

Let's do some metaclass magic python programming.

class InheritanceMetaclass(ModelBase):
 def __call__(cls, *args, **kwargs):
  obj = super(InheritanceMetaclass, cls).__call__(*args, **kwargs)
  obj.__class__ = obj._get_class()
  return obj


class Animal(models.Model):
 __metaclass__ = InheritanceMetaclass

 TYPES=( ('AnimalDog', 'Dog'), 
   ('AnimalCat', 'Cat'), 
   ('AnimalCow', 'Cow'))

 name = models.CharField(max_length=15)
 type = models.CharField(max_length=15, choices=TYPES)
 
 def __unicode__(self):
  return self.name

 def make_sound(self):
  print "Some sound"

 def _get_class(self):
  if not self.type:
   return self.__class__
  else:
   return getattr(sys.modules[self.__module__], self.type)

class AnimalDog(Animal):
 def make_sound(self):
  print "Hauu!"
 class Meta:
  proxy = True

class AnimalCat(Animal):
 def make_sound(self):
  print "Meow!"
 class Meta:
  proxy = True

class AnimalCow(Animal):
 def make_sound(self):
  print "Mooo!"
 class Meta:
  proxy = True
  
  

Drop all from the database for previous example and create new models. Then try this:

>>> Animal(name='Cattie', type='AnimalCat').save()
>>> Animal(name='Doggie', type='AnimalDog').save()
>>> Animal(name='Cowwie', type='AnimalCow').save()
>>> Animal.objects.all()
[<AnimalCat: Cattie>, <AnimalDog: Doggie>, <AnimalCow: Cowwie>]
>>> for a in Animal.objects.all():
...    a.make_sound()
... 
Meow!
Hauu!
Mooo!

Background notes:

  • because we create proxy models, there is only one database table for the Animal,
  • every time you query for Animal object you get an object casted to appropriate class (not the Animal), but in this same time this will work flawless with django, because of duck typing. AnimalDog will behave exactly the same as parent Animal class in case of using it in django admin and so on,
  • we store in the attribute Animal.type a name of django class model name, 
  • we can easily extend our Animal types by adding new values to Animal.TYPES,
  • if you need real model inheritance (e.g. add custom model fields depending on a model type) use django model inheritance - this is not described here case,
  • you can also store type as Integer field, but then it needs some further hacking in _get_class() method, see also how to avoid hardcoding with Perfect choice.

Friday, April 1, 2011

Perfect choice

Have you ever wondered how to deal with django choices without hardcoding numeric values into code?


Well, I was wondering too, found some solutions on the Web but after all need some rewriting to came up with this:

class Enumeration(object):
 """
 A small helper class for more readable enumerations,
 and compatible with Django's choice convention.
 You may just pass the instance of this class as the choices
 argument of model/form fields.

 Example:
  MY_ENUM = Enumeration([
   (100, 'MY_NAME', 'My verbose name'),
   (200, 'MY_AGE', 'My verbose age'),
  ])
  assert MY_ENUM.MY_AGE == 100
  assert MY_ENUM[1] == (200, 'My verbose age')
 """

 def __init__(self, enum_list):
  self.enum_list_full=enum_list
  self.enum_list = [(item[0], item[2]) for item in enum_list]
  self.enum_dict = {}
  self.enum_display = {}
  for item in enum_list:
   self.enum_dict[item[1]] = item[0]
   self.enum_display[item[0]] = item[2]

 def __contains__(self, v):
  return (v in self.enum_list)

 def __len__(self):
  return len(self.enum_list)

 def __getitem__(self, v):
  if isinstance(v, basestring):
   return self.enum_dict[v]
  elif isinstance(v, int):
   return self.enum_list[v]

 def __getattr__(self, name):
  return self.enum_dict[name]

 def __iter__(self):
  return self.enum_list.__iter__()
 def __repr__(self):
  return 'Enum(%s)' % self.enum_list_full.__repr__()

 def get_display_name(self, v):
  return self.enum_display[v]

This is a little customization of this snippet: http://djangosnippets.org/snippets/1647/

How does it work with models? Nice and easy:

class SoomeModel(models.Model):
 OPTIONS=Enumeration([
   (1, OPTION_A, u'This is option A'),
   (2, OPTION_B, u'This is option B'),
   (3, OPTION_C, u'This is option C'),

  ])
 option = models.IntegerField(choices=OPTIONS)

Now you can use it like that:

>> SomeModel.OPTIONS.OPTION_A
.. 1
>> SomeModel.OPTIONS['OPTION_B']
.. 2
>> SomeModel.OPTIONS[1]
.. (2, 'This is option B')
>> SomeModel.OPTIONS.get_display_name(2)
.. "This is option B"
>> SomeModel.OPTIONS.get_display_name(SomeModel.OPTIONS_A)
.. "This is option A"

No more hardcoding values. No more!

Kick off

Yeap - it is April fools. Lovely time to start blogging :-)