lunes, 24 de octubre de 2011

Diferentes páxinas para dispositivos móviles e fixos

Problema

Temos páxinas que se leen con dificultade en dispositivos móviles, como iPhone e Android. Buscando por ahí adiante atopéi varios enfoques, e o máis extendido é analizar a petición do cliente (HTTP_USER_AGENT sobre todo) para ver si pertence a un dispositivo móvil. Unha vez determinado, aplicar a solución requerida

A base

Partín da base de Django mobile utils, que implementa unha solución robusta. Cando chega a petición (request), antes de chegar ó views.py pasa por un middleware que intenta averiguar si o USER_AGENT é móbil. A función está no __init__.py, e garda o request nunha variable local. Aquí é donde me falla, xa que non consigo recuperar as variables locais. Despóis de pasar polo middleware, o request orixinal está marcado como .is_mobile = True. Hai un código para cargar os "templates" alternativos, dentro de loaders.py; este loader mira dentro da variable local de request si ten o valor .is_mobile == True, porque non se lle pode pasar como parámetro o propio request. Así que si falla o request local, falla todo.
Por sorte, o autor tamén incluiu un context_processor, que simplemente se encarga de pasarlle un diccionario de variables ós templates. Por exemplo, si activamos o context_procesor de auth, en tódolos templates teremos dispoñibles un número de variables como user.

A solución

Utilizando o código anterior, podemos simplificalo para utilizar só o __init__.py e o context_processor.py. Os context_processors deben manterse simples según o manual oficial, así que si podemos pasar só unha variable, mellor que dúas. Dende o context_processor.py orixinal:
def mobile_browser(request):
    dict = {'mobile_browser': False}
    if hasattr(request, 'is_mobile'):
        dict['mobile_browser'] = request.is_mobile
    return dict
Podemos pasar a outro context máis simple aínda, como:
def mobile_browser(request):
    dict = {'mobile_browser': False}
    if our_server.django_mobile_utils.is_mobile(request):
        dict['mobile_browser'] = True
    return dict
Vemos que o autor orixinal sigue tirando de request acumulado en local, pero nós imos testear directamente o request. Modificamos tamén o código do __init__.py. Das varias liñas de tipo:
request.mobile = True
return request
Cambiamos a:
return True
E a última liña que pon return request cambiámola a return False. Ou adornamos con variables, si nos gusta máis.
Si estamos pola simplificación extrema, todas as globáis deste archivo pódense eliminar, así como a configuración no settings.py, no que chega con engadir:
TEMPLATE_CONTEXT_PROCESSORS = (
    "our_server.django_mobile_utils.context_processors.mobile_browser",
    ...

Aplicación

Agora temos no directorio django_mobile_utils os arquivos __init__.py, context_processor.py e un directorio data/mobile_agents.txt, e o settings.py modificado como corresponde. ¿Cómo ó usamos? Simplemente, nos templates utilizamos a variable booleana {{mobile_browser}} como condicional, por exemplo para cargar unha folla de estilo diferente:
{% if mobile_browser %}
  <link href="/media/css/mobile_style.css" rel="stylesheet" type="text/css" />
{% else %}
  <link href="/media/css/style.css" rel="stylesheet" type="text/css" />
{% endif %}

miércoles, 13 de julio de 2011

Conseguir a secuencia de un PDB

Problema



Temos un archivo .pdb, e queremos extraer a secuencia de aminoácidos de unha soa letra. Atopei algunha solución por internet, pero implica importar a librería Bio.PDB.Polypeptide.PPBuilder, e resultábame un método algo escuro. Así que busquei ata atopar o diccionario que traduza código de tres letras a código de unha letra.

Librerías



from Bio.SCOP.Raf import to_one_letter_code
from Bio.PDB import PDBParser


O diccionario está algo escondido, o módulo Raf (ASTRAL RAF (Rapid Access Format) Sequence Maps) é a primeira vez que me cruzo con él. Penso que debería estar referenciado en Bio.Alphabet.IUPAC un diccionario similar de traducción tres<->unha letras.

Código



Creamos un "motor" para parsear archivos PDB (p), parseamos o archivo en s e logo extraemos a secuencia dese archivo s a unha secuencia y, que traducimos con to_one_letter_code:

p = PDBParser()

s = p.get_structure("id", "/path/to/file.pdb")
y = [x.get_resname() for x in s.get_residues()]
sequence = "".join([to_one_letter_code[z] for z in y])

¡Listo! En sequence temos un string coa secuencia do PDB. Non sei si é máis lento ou máis ineficiente que o método do PPBuilder, pero desde logo é o método que eu estaba buscando: sacar a secuencia tri-letra do PDB e traducila. Haberá outras situacións nas que teñas unha secuencia tri-letra que queiras levar a unha letra sin ter que chamar polo PPBuilder. Nese caso, importando o diccionario e facéndolle:

[to_one_letter_code[z] for z in y]

Xa o teremos.

lunes, 9 de mayo de 2011

A API do NCBI

A veces teremos que consultar de forma masiva o NCBI. O normal é baixarse un xenoma que está en miles de trozos, ou que nun artigo nos digan que subiron as secuencias AE000001-AE001000.

Imos facer un pequeniño script para facer este tipo de consultas fácilmente. Primeiro conseguimos os id de referencia. Por exemplo, si queremos baixar as secuencias de Aedes aegypti, miramos no artigo http://www.ncbi.nlm.nih.gov/pubmed/17510324, que nos dí que na entrada AAGE00000000 está o xenoma. Consultando nesa entrada vemos que o WGS está en AAGE02000001-AAGE02036206.

import time
import urllib, urllib2

root = "AAGE020"
end = 36206

efetch = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"

output = open("AAGE02000000.fas", "w")

for i in ["{0:05}".format(x) for x in range(1, end)]:
params = {"db": "nucleotide",
"id": root + i,
"rettype": "fasta"}
GET_params = urllib.urlencode(params)
page = urllib2.urlopen("{url}?{GET}".format(url=efetch, GET=GET_params))

output.write(page.read())

time.wait(1)

output.close()


A liña ["{0:05}".format(x) for x in range(1, end)] sólo prepara o número da entrada con "0" á esquerda ata completar unha lonxitude de 5 números ("00001", "00134", etc).

Datos con ids non consecutivas



Xogando un pouco coas APIs de NCBI, cos valores da base de datos e coas ids, podemos pedir calquer dato do NCBI. Si queremos datos non consecutivos ou que teñamos nunha lista, por exemplo unha lista de PMIDs, modificamos así o script:

import time
import urllib, urllib2

my_querys = ["19226459", "18269752", "17725574", "17507679", "17203060",
"16174338", "15797612", "15545653", "15219154", "14738738",
"12716978", "12361573", "12032249", "11732090", "11108594",
"10893864"]

efetch = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"

output = open("articles.txt", "w")

for i in my_querys:
params = {"db": "pubmed",
"id": i,
"rettype": "abstract",
"retmode": "html"}
GET_params = urllib.urlencode(params)
page = urllib2.urlopen("{url}?{GET}".format(url=efetch, GET=GET_params))

output.write(page.read())

time.wait(1)

output.close()


A lista que cargamos en my_querys" podémola obter de calquera fonte: si temos unha base de datos de bibliografía, é moi fácil exportar os PMID ou sacalos parseando un volcado desa base de datos a un formato de texto plano.

Be polite with NCBI



Si temos pensado baixar un número ENORME de datos, é mellor facer primeiro unha búsqueda, e cos datos da búsqueda pedir as secuencias. O NCBI prefire que lle consultemos os datos desta forma, de tal modo que pode banearnos a IP si llos pedimos masivamente como víamos no primeiro snippet.

import time
import urllib, urllib2
from xml.dom import minidom

term = "AAGE02000000"

esearch = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
efetch = "http://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"

search = urllib2.urlopen(esearch+ "?" + \
urllib.urlencode({"db": "nuccore",
"term": term,
"usehistory": "yes"}))

xml = minidom.parse(search)
webenv = xml.getElementsByTagName("WebEnv")[0].firstChild.data
q_key = xml.getElementsByTagName("QueryKey")[0].firstChild.data
count = xml.getElementsByTagName("Count")[0].firstChild.data

output = open("AAGE02000000.fas", "w")

ret_max = 500
for ret_start in range(0, int(count), ret_max):

params = {"db": "nuccore",
"WebEnv": webenv,
"query_key": q_key,
"retstart": ret_start,
"retmax": ret_max,
"rettype": "fasta"}
GET_params = urllib.urlencode(params)
page = urllib2.urlopen("{url}?{GET}".format(url=efetch, GET=GET_params))

output.write(page.read())

output.close()


No parámetro "term" podemos meter unha liña de texto coa que fagamos calquera búsqueda, coa sintaxis propia do NCBI. Máis en NCBI - Entrez, pero en perl.

viernes, 29 de abril de 2011

Generic views con ForeignKey(User)

Con generic views podemos facer os accesos máis comúns á base de datos (engadir, borrar, editar, consultas...). A veces haberá que facer pequenos engadidos de código para conseguir axustes. Hoxe vamos ver cómo subir archivos ó servidor sin perder o trazo dos donos deses archivos.

models.py


Queremos aceptar un archivo que o usuario sube, e que o usuario lle dea unha descripción ó archivo. Comprobar que ese archivo sea seguro (que non nos suban un script, por exemplo) é outra tarea distinta que queda nas mans do programador.

from django import forms
from django.db import models
from django.forms import ModelForm
from django.contrib.auth.models import User
import uuid

def set_path(my_model, file_name):
'''Sets the file_path for the uploaded file'''
return "uploads/{0}".format(uuid.uuid1())

# Create your models here.
class OurModel(models.Model):
name = models.CharField(max_length=50)
user = models.ForeignKey(User, editable=False)
file_path = models.FileField(upload_to=set_path, blank=True)

def __unicode__(self):
return self.name

class OurModelForm(ModelForm):

class Meta:
model = OurModel
exclude = ("user",)


urls.py



Estando dentro da nosa aplicación, empezaremos necesitando a vista de list_detail.object_detail e a de create_update.create_object. Como imos a modificar aparte de crear o obxeto, a vista xenérica de create_object hai que desplazala a unha función de views.py (será add).

from django.conf.urls.defaults import *
from models import OurModel, OurModelForm

import views

urlpatterns = patterns('',
url(r'^(?P<object_id>\d+)/$',
"django.views.generic.list_detail.object_detail",
dict(queryset=OurModel.objects.all()),
name='ourmodel_detail'),
url(r'^add/$',
views.add,
name='ourmodel_add'),
)


views.py



Aquí creamos a función add que se chama desde urls.py. Nesta función trataremos as chamadas GET e POST, xa que o formulario ten o "action" cara sí mesmo. Si se chama vía GET, devolvemos a vista de create_object, si se chama vía POST, instanciamos o modelo cos datos que nos veñen do formulario, instanciamos un usuario (User) co usuario que está logueado e pasámoslle ambas instancias ó modelo, para que as grave.

O "truco" está en cargar o formulario e preparalo para grabar (commit=False), xa que a partir deste momento poderemos cargarlle tamén a instancia de "User". Despois da grabación, podemos redirixir o usuario cara calquer páxina, aquí mandámolo a un object_detail do recén grabado.

from django.views.generic.create_update import create_object
from django.views.generic.list_detail import object_detail
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User

from models import OurModel, OurModelForm

@login_required
def add(request):
if request.method == "POST":
form = OurModelForm(data=request.POST,
files=request.FILES)
if form.is_valid():
our_model = form.save(commit=False)
our_model.user = User.objects.get(pk = request.user.id)
our_model.save()

return object_detail(request,
OurModel.objects.all(),
object_id=align.id)

return create_object(request,
model=OurModel,
form_class=OurModelForm,
login_required=True)


templates



Necesitamos só dous templates, /our_app/ourmodel_detail.html e /our_app/ourmodel_form.html. Con un pequeno cambio: os formularios que suben archivos teñen unha propiedade añadida

<form enctype="multipart/form-data" method="post" action="."></form>


Listo!

Resultado



Cando un usuario rexistrado accede á páxina http://ourserver.com/ourapp/add/ por primeira vez (vía GET), o urls.py lévao á función def add de views.py. Nesa función sáltase toda a parte "non POST", e simplemente saca o generic view para create_update.create_object, que presenta o formulario indicado.

Si o usuario enche o formulario correctamente (se non o fai, o generic view automáticamente volve para atrás), chámase a función add de novo, pero agora vía POST e cos datos apropiados. Agora entramos no proceso propio de grabar o modelo na base de datos cos datos apropiados.

miércoles, 6 de abril de 2011

Django con generic views

Nota: generic_views a partir de Django 1.3 compórtanse como clases, tal e como indican no manual (migración)

Imos progresando con Django. Onte descubrín as generic_views, e de cómo levo facendo o "mono" creando funcións en views.py para cargar os modelos da base de datos. generic_views veñen na instalación de Django en django.views.generic, así que abrimos unha consola python/django
python manage.py shell
>>>import django.views.generic
>>>help(django.views.generic)

Obtendo a documentación da nosa instalación. Na miña sae:
PACKAGE CONTENTS
    create_update
    date_based
    list_detail
    simple

E así sabemos qué librerías de generic_views temos dispoñibles. Podemos importar estas librerías para saber cómo funcionan, e pedir axuda igual que antes:
python manage.py shell
>>>import django.views.generic.create_update
>>>help(django.views.generic.create_update)

Para o exemplo utilizaremos create_object, función que engade unha entrada na base de datos baseándose no modelo.

Modelo


/elements/models.py

from django.db import models

class Content(models.Model):
    family_choices = (
        ("A", "A family"),
        ("B", "B family"),
        ("C", "C family"),
    )

    name = models.CharField(max_length=50)
    family = models.CharField(max_length=8, choices=family_choices)
    subfamily = models.CharField(max_length=50)
    content = models.CharField(max_length=32767)
    add_date = models.DateTimeField()
    def __unicode__(self):
        return self.name

Template


O template, según vimos na axuda help(django.views.generic.create_update) debe estar ubicado de forma que o Django o atope. A documentación decía Templates: "app_label/model_name_form.html", que no noso caso sería elements/content_form.html.

<form method="post" action=".">
{{ form.as_p }}
<input type="submit" />
</form>

Como é normal, podemos envolver este formulario en calquer {% block %} que entre ben no noso deseño.

Urls


Nas urls.py está a clave da cuestión. No urls.py base poñeremos:
(r'^elements/', include('noso_app.elements.urls'))
E en elements/urls.py poñeremos:

from noso_app.elements.models import Content
...
(r'^add/$', 'django.views.generic.create_update.create_object', dict(model=Contents, post_save_redirect="/elements/"))

O parámetro "post_save_redirect" será a url á que iremos despóis de facer o gardado na base de datos.

Resultado


Agora cando entremos no noso dominio.com/elements/add, sairanos un formulario que automáticamente gardará as entradas na base de datos. Parece complicado, pero si nos fixamos estamos evitando calquer interacción directa coa base de datos, nin temos que escribir ningún views.py para manexar a entrada de datos. Si navegamos na documentación de generic_views atoparemos atallos para listar, borrar, actualizar... sin picar case nada de código.

miércoles, 16 de marzo de 2011

Prototype + JSON + HTML

Hoxe sí que rompín os cornos cunha nadería. Question is, temos un cálculo longo nun clúster, e queremos ir actualizando a páxina web según vaian saíndo os resultados, vía Ajax (prototype). Ahí vai o script no lado do cliente:


function test_job(){
var html_target = $('status'); /* modificaremos o <div id="status"></div>*/
var html_target2 = $('results'); /* cando rematemos, modificamos <div id="results"></div>*/

var job_list = {tasks: {[1, 2, 3, 4]}; /*estas son os pid no cluster, pódense xerar dinámicamente*/

job_state = new Ajax.PeriodicalUpdater("", '/testcluster/'){

/* O servidor web atende as peticións en http://noso_server.com/testcluster/ */
/* No lado do servidor, éste pregunta ó clúster como van as tareas de job_list */
/* que responderá cun diccionario de "running", "delayed" e "finished". */
/* Con esta resposta, o servidor xenera un contido "html" para ver no */
/* <div id="status"></div>, e pásallo ó cliente por JSON */
/* A clave está en pasarlle a PeriodicalUpdater un item vacío, porque senón */
/* actualizaríao con todo o paquete JSON que se recibe */

method: 'post',
content_type: "application/json",
parameters: job_list,
frequency: 5,
decay: 1,
onSuccess: function(transport){
json_content = transport.responseJSON;
pending_jobs = json_content["running"].length;
delayed_jobs = json_content["delayed"].length;
html_target.update(json_content["html"]);
if((pending_jobs + delayed_jobs) == 0 ){
job_state.stop(); /*Xa non quedan máis jobs, paramos o Updater */
new Ajax.Request('/getresults/', {
onSuccess: function(response){
html_target2.update(response.responseText);
}
});
}
}
});
}

/* E con esta liña lanzamos o Periodical updater tan pronto se carga a páxina */
Event.observe(window, 'load', test_job, false);