Wpisy z kategorii: django

Team of experienced Django devs looking for remote contract work

Three month ago I decided to become a freelancer. As of 31 January 2011 I stopped working for Open-E company where I spent last 3 years as Python/Django developer.

Now I join forces together with my friend Bartosz Burclaf under name of eFabryka.com. We are a small, agile and goal oriented, distributed developers team operating out of Poland.

In our daily work we strongly believe in KISS and DRY principles. While designing and writing our code we follow TDD/BDD approach.

We are looking for interesting projects to dive into - find out more on: www.efabryka.com

django-moderation new release 0.2 - status update

Today i have released new version of django-moderation - 0.2. It has been uploaded in to pypi, so now it can be installed with easy_install.

Many bugs were fixed and many new features were added since initial release.

List of changes

  • Added GenericModerator class that encapsulates moderation options for a given model.Changed register method, it will get only two parameters: model class and settings class.
  • Added option to register models with multiple managers.
  • Added options to GenericModerator class: auto_approve_for_superusers, auto_approve_for_staff, auto_approve_for_groups, auto_reject_for_anonymous, auto_reject_for_groups. Added methods for checking auto moderation.
  • Added automoderate helper function.
  • Changed moderated_object property in ModerationManager class, moderated object is get only once from database, next is cached in moderatedobject, fixed issue with not setting user object on changed_by attribute of ModeratedObject model.
  • Fixed issue when loading object from fixture for model class that is registered with moderation. Now moderated objects will not be created when objects are loaded from fixture.
  • Fixed issue with TypeError when generating differences of changes between model instances that have field with non unicode value ex. DateField.
  • Fixed issue with accessing objects that existed before installation of django-moderation on model class.
  • Fixed issue when more then one model is registered with moderation and multiple model instances have the same pk.
  • Fixed issue with multiple model save when automoderate was used. Auto moderation in save method of ModeratedObject has been moved to separate method.
  • Added admin filter that will show only content types registered with moderation in admin queue.
  • Fixed issue when creating model forms for objects that doesn't have moderated object created.
  • Added possibility of passing changed object in to is_auto* methods of GenericModerator class. This will allow more useful custom auto-moderation. Ex. auto reject if akismet spam check returns True.
  • Added ability to provide custom auto reject/approve reason.
  • Added option bypass_moderation_after_approval in to GenericModerator class that will release object from moderation system after initial approval of object.
  • Other bug fixes and code refactoring.

I would like to thank all persons that had contributed to this project, found bugs, submitted ideas, send patches.

Special thanks to jonwd7 - Thank you for all ideas, bug fixes, patches.

Thank you all very much.

If any body will find any new bugs, please create an issue at github

Announcing django-moderation

django-moderation is reusable application for Django framework, that allows to moderate any model objects.

Code can be found at http://github.com/dominno/django-moderation

Possible use cases:

  • User creates his profile, profile is not visible on site. It will be visible on site when moderator approves it.
  • User changes his profile, old profile data is visible on site. New data will be visible on site when moderator approves it.

Features:

  • configurable admin integration(data changed in admin can be visible on site when moderator approves it)
  • moderation queue in admin
  • html differences of changes between versions of objects
  • configurable email notifications
  • custom model form that allows to edit changed data of object
  • 100% PEP8 correct code
  • test coverage > 80%

Requirements

python >= 2.4

django >= 1.1

Installation

Download source code from http://github.com/dominno/django-moderation and run installation script:

$> python setup.py install

Configuration

  1. Add to your INSTALLED_APPS in your settings.py:

    moderation

  2. Run command manage.py syncdb

  3. Register Models with moderation

    from django.db import models
    from moderation import moderation
    
    
    class YourModel(models.Model):
        pass
    
    moderation.register(YourModel)
    
  4. Register admin class with your Model

    from django.contrib import admin
    from moderation.admin import ModerationAdmin
    
    
    class YourModelAdmin(ModerationAdmin):
        """Admin settings go here."""
    
    admin.site.register(YourModel, YourModelAdmin)
    

If you want to disable integration of moderation in admin, add admin_intergration_enabled = False to your admin class:

class YourModelAdmin(ModerationAdmin):
    admin_intergration_enabled = False

admin.site.register(YourModel, YourModelAdmin)

How django-moderation works

When you change existing object or create new one, it will not be publicly available until moderator approves it. It will be stored in ModeratedObject model.

your_model = YourModel(description='test')
your_model.save()

YourModel.objects.get(pk=your_model.pk)
Traceback (most recent call last):
DoesNotExist: YourModel matching query does not exist.

When you will approve object, then it will be publicly available.

your_model.moderated_object.approve(moderatated_by=user,
                                   reason='Reason for approve')

YourModel.objects.get(pk=1)
<YourModel: YourModel object>

You can access changed object by calling changed_object on moderated_object:

your_model.moderated_object.changed_object
<YourModel: YourModel object>

This is deserialized version of object that was changed.

Now when you will change an object, old version of it will be available publicly, new version will be saved in moderated_object

your_model.description = 'New description'
your_model.save()

your_model = YourModel.objects.get(pk=1)
your_model.__dict__
{'id': 1, 'description': 'test'}

your_model.moderated_object.changed_object.__dict__
{'id': 1, 'description': 'New description'}

your_model.moderated_object.approve(moderatated_by=user,
                                   reason='Reason for approve')

your_model = YourModel.objects.get(pk=1)
your_model.__dict__
{'id': 1, 'description': 'New description'}

Email notifications

By default when user change object that is under moderation, e-mail notification is send to moderator. It will inform him that object was changed and need to be moderated.

When moderator approves or reject object changes then e-mail notification is send to user that changed this object. It will inform user if his changes were accepted or rejected and inform him why it was rejected or approved.

How to overwrite email notification templates

E-mail notifications use following templates:

  • moderation/notification_subject_moderator.txt
  • moderation/notification_message_moderator.txt
  • moderation/notification_subject_user.txt
  • moderation/notification_message_user.txt

Default context:

content_type - content type object of moderated object

moderated_object - ModeratedObject instance

site - current Site instance

How to pass extra context to email notification templates

If you want to pass extra context to email notification methods you new need to create new class that subclass BaseModerationNotification class.

class CustomModerationNotification(BaseModerationNotification):
    def inform_moderator(self,
                     subject_template='moderation/notification_subject_moderator.txt',
                     message_template='moderation/notification_message_moderator.txt',
                     extra_context=None):
        '''Send notification to moderator'''
        extra_context={'test':'test'}
        super(CustomModerationNotification, self).inform_moderator(subject_template,
                                                                   message_template,
                                                                   extra_context)

    def inform_user(self, user,
                    subject_template='moderation/notification_subject_user.txt',
                    message_template='moderation/notification_message_user.txt',
                    extra_context=None)
        '''Send notification to user when object is approved or rejected'''
        extra_context={'test':'test'}
        super(CustomModerationNotification, self).inform_user(user,
                                                              subject_template,
                                                              message_template,
                                                              extra_context)

Next register it with moderation as notification_class:

moderation.register(YourModel, notification_class=CustomModerationNotification)

Signals

moderation.signals.pre_moderation - signal send before object is approved or rejected

Arguments sent with this signal:

sender - The model class.

instance - Instance of model class that is moderated

status - Moderation status, 0 - rejected, 1 - approved

moderation.signals.post_moderation - signal send after object is approved or rejected

Arguments sent with this signal:

sender - The model class.

instance - Instance of model class that is moderated

status - Moderation status, 0 - rejected, 1 - approved

Forms

When creating ModelForms for models that are under moderation use BaseModeratedObjectForm class as ModelForm class. Thanks to that form will initialized with data from changed_object.

from moderation.forms import BaseModeratedObjectForm


class ModeratedObjectForm(BaseModeratedObjectForm):

    class Meta:
        model = MyModel

Any comments ? Feedback ? Feature requests ?

Jak sprawdzać zgodność kodu Pythona z PEP8

Znalazłem ostatnio bardzo fajne narzędzie do sprawdzania czy kod Pythona jest zgodny z zalecanym stylem pisania kodu w Pythonie(PEP8). Jest to skrypt uruchamiany z konsoli który sprawdza czy wskazany moduł jest zgodny z PEP8.

Przykład użycia:

$ pep8.py --filename=*.py --show-source --show-pep8 /sciezka/do/projektu/
parts/djangopl/sitemaps.py:14:5: E301 expected 1 blank line, found 0
def changefreq(self, obj):
^
Separate top-level function and class definitions with two blank lines.

Method definitions inside a class are separated by a single blank line.

Extra blank lines may be used (sparingly) to separate groups of related
functions.  Blank lines may be omitted between a bunch of related
one-liners (e.g. a set of dummy implementations).

Use blank lines in functions, sparingly, to indicate logical sections.
...

parametry:

  • filename - wzorzec jakie pliki mają być brane pod uwagę
  • show-source - przy każdym błędzie będzie wypisany kawałek kodu i wskazane w którym miejscu jest błąd
  • show-pep8 - do każdego błędu będzie podane wyjaśnienie z dokumentu PEP8

Wynik działania prezentuje się bardzo ładnie i można szybko wychwycić gdzie jest błąd i zobaczyć dlaczego tak nie powinno się pisać kodu w Pythonie.

Znalazłem też opis jak zintegrować sprawdzanie zgodności z PEP8 z testami wykorzystywanymi w systemie typu continuous integration:

http://www.ajaxline.com/continuous-integration-in-django-project

Przydatne linki związane z PEP8:

Django - migracje z south

South jest jedną z najlepszych aplikacji do prostego tworzenia migracji struktur baz danych w Django. Używam south'a od wersji 0.3. I muszę powiedzieć że jest to naprawdę dobre narzędzie. Ułatwia prace przy tworzeniu i rozwijaniu aplikacji w Django. Normalnie gdy tworzymy aplikacje w Django i korzystamy z syncdb, gdy chcemy zmienić strukturę bazy danych to musimy albo usunąć całą bazę i ponownie wykonać syncdb lub pisać "ręcznie" SQL który zmieni tą strukturę. Dodatkowym problemem jest jeżeli chcemy później te same zmiany struktury bazy danych wykonać na środowisku produkcyjnym. Przy większych projektach jest bardzo uciążliwe i zajmuje bardzo dużo czasu. Chciałbym opisać tutaj swoje doświadczenia z south 0.5 i pokazać jego możliwości.

Instalacja

Jeżeli nie mamy jeszcze zainstalowanego south'a to instalujemy:

easy_install south

Zaczynamy:

Tworzymy projekt który posiada dwie aplikacje books oraz authors.

django-admin.py startproject southtest
manage.py startapp books
manage.py startapp authors

Dopisujemy south oraz aplikacje books i authors do INSTALLED_APPS i robimy syncdb. South stworzy sobie tabele w której będzie trzymać historie migracji.

Dopisujemy modele do aplikacji books i authors

Modele z aplikacji authors:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=30)

Modele z aplikacji books:

from django.db import models
from southtest.authors.models import Author

class Book(models.Model):
    name = models.CharField(max_length=30)
    author = models.ForeignKey(Author)

Na początku musimy stworzyć pierwsze migracje dla tych dwóch aplikacji:

./manage.py startmigration books --initial
+ Added model 'books.Book'
Created 0001_initial.py.

./manage.py startmigration authors --initial
+ Added model 'books.Book'
Created 0001_initial.py.

South automatycznie stworzy moduł migrations w katalogu aplikacji dla której tworzona jest migracja. W module tym będą trzymane pliki migracji. Stworzone zostaną także migracje które dodadzą tabele reprezentujące modele z obydwu aplikacji. Każda migracja posiada metodę forwards która wykonuje migracje oraz metodę backwards która wycofuje migracje.

Migracja dla modelu Book będzie wyglądać następująco:

from southtest.books.models import *

class Migration:

    def forwards(self, orm):

        # Adding model 'Book'
        db.create_table('books_book', (
            ('author', models.ForeignKey(orm['authors.Author'])),
            ('id', models.AutoField(primary_key=True)),
            ('name', models.CharField(max_length=30)),
        ))
        db.send_create_signal('books', ['Book'])

    def backwards(self, orm):

        # Deleting model 'Book'
        db.delete_table('books_book')

    models = {
        # ... tutaj zamrożony stan bazy danych dla aplikacji books,  w momencie tworzenia migracji
    }

    complete_apps = ['books']

Migracja dla modelu Author:

from south.db import db
from django.db import models
from southtest.authors.models import *

class Migration:

    def forwards(self, orm):

        # Adding model 'Author'
        db.create_table('authors_author', (
            ('id', models.AutoField(primary_key=True)),
            ('name', models.CharField(max_length=30)),
        ))
        db.send_create_signal('authors', ['Author'])

    def backwards(self, orm):

        # Deleting model 'Author'
        db.delete_table('authors_author')

...

South na końcu każdej migracji tworzy stan w jakim znajduje struktura bazy danych dla aplikacji dla której jest migracja. Na tej podstawie potrafi automatycznie przy tworzeniu kolejnej migracji wykryć jakie zmiany zostały dokonane na modelu i stworzyć odpowiednią migracje.

Teraz wykonujemy nasze migracje:

./manage.py migrate
Running migrations for books:
 - Migrating forwards to 0001_initial.
 > books: 0001_initial
   = CREATE TABLE `books_book` (`author_id` integer NOT NULL, `id` integer
   AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(30) NOT NULL); []
   = ALTER TABLE `books_book` ADD CONSTRAINT `author_id_refs_id_5ab0fe2d` 
   FOREIGN KEY (`author_id`) REFERENCES `authors_author` (`id`); []
   = CREATE INDEX `books_book_author_id` ON `books_book` (`author_id`); []
 - Sending post_syncdb signal for books: ['Book']
 - Sending post_syncdb signal for books: ['Book']
 - Loading initial data for books.
Running migrations for authors:
 - Migrating forwards to 0001_initial.
 > authors: 0001_initial
   = CREATE TABLE `authors_author` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` varchar(30) NOT NULL); []
 - Sending post_syncdb signal for authors: ['Author']
 - Sending post_syncdb signal for authors: ['Author']
 - Loading initial data for authors.

South wykonał nam wszystkie migracje dla aplikacji dla których mamy stworzone migracje.

Jeżeli mamy jakąś aplikacje na której zostało już wykonane syncdb i tabele już istnieją, a chcemy załączyć dla niej obsługę migracji to tworzymy dla niej pustą migracje za pomocą:

./manage.py startmigration inna_applikacja --initial

Następnie wykonujemy:

./manage.py migrate inna_applikacja --fake

South zapamięta dzięki temu obecny stan bazy danych.

Automatycznie tworzenie migracji

Do modelu book dodajemy nowe pole - category, które będzie kluczem obcym do modelu Category oraz pole published_date. Model Book wygląda teraz tak:

from django.db import models
from southtest.authors.models import Author

class Category(models.Model):
    name = models.CharField(max_length=20)

class Book(models.Model):
    name = models.CharField(max_length=30)
    author = models.ForeignKey(Author)
    category = models.ForeignKey(Category)
    published_date = models.DateField(auto_now_add=True)

Tworzymy nową migracje:

./manage.py startmigration books add_category_and_published_date --auto
+ Added model 'books.category'
+ Added field 'books.book.category'
+ Added field 'books.book.publiched_date'
Created 0002_add_category_and_published_date.py.

South stworzył plik migracji o nazwie 0002_add_category_and_published_date.py. South wykrył że dodaliśmy dwa nowe pola wygenerował odpowiednie migracje. Plik migracji wygląda tak:

from southtest.books.models import *

class Migration:

    def forwards(self, orm):

        # Adding model 'Category'
        db.create_table('books_category', (
            ('id', models.AutoField(primary_key=True)),
            ('name', models.CharField(max_length=20)),
        ))
        db.send_create_signal('books', ['Category'])

        # Adding field 'Book.category'
        db.add_column('books_book', 'category', models.ForeignKey(orm.Category))

        # Adding field 'Book.publiched_date'
        db.add_column('books_book', 'publiched_date', models.DateField(auto_now_add=True))

    def backwards(self, orm):

        # Deleting model 'Category'
        db.delete_table('books_category')

        # Deleting field 'Book.category'
        db.delete_column('books_book', 'category_id')

        # Deleting field 'Book.publiched_date'
        db.delete_column('books_book', 'publiched_date')

South wygenerował nam migracje która stworzy tabele books_category, doda pola category_id oraz publiched_date do tabeli books_book.

Wycofywanie migracji

South umożliwia także wycofywanie migracji do dowolnego stanu bazy danych. Należy tu pamiętać aby jeżeli piszemy migracje "ręcznie", zawsze pisać do nich migracje wycofujące zmiany, gdyż nigdy nie wiadomo czy w czasie pracy nad projektem nie będziemy chcieli wrócić do jakiegoś konkretnego stanu bazy danych.

Aby wycofać do jakiegoś konkretnego stanu bazy danych, używamy polecenia migrate podając nazwę aplikacji oraz nazwę migracji do której chcemy się cofnąć, np:

./manage.py migrate books 0001_initial
Running migrations for books:
 - Migrating backwards to just after 0001_initial.
 < books: 0002_add_category_and_published_date
   = DROP TABLE `books_category` CASCADE; []
   = ALTER TABLE `books_book` DROP COLUMN `category_id` CASCADE; []
   = ALTER TABLE `books_book` DROP COLUMN `publiched_date` CASCADE; []

W tym przykładzie south cofnie się do stanu bazy danych po zaaplikowaniu migracji 0001_initial. Aby całkowicie usunąć wszystkie migracje dla danej aplikacji, używamy:

./manage.py migrate books zero
Running migrations for books:
 - Migrating backwards to just after zero.
 < books: 0001_initial
   = DROP TABLE `books_book` CASCADE; []

Migracje danych

Z migracjami struktur baz danych wiążą się także migracje danych.

Tworzymy szablon migracji do którego wstawimy nasze migracje danych.

./manage.py startmigration books change_category_name
Created 0003_change_category_name.py.

Stworzony został plik migracji:

from south.db import db
from django.db import models
from southtest.books.models import *

class Migration:

    def forwards(self, orm):
        "Write your forwards migration here"

    def backwards(self, orm):
        "Write your backwards migration here"

    models = {
        'books.book': {
            'author': ('models.ForeignKey', ['Author'], {}),
            'category': ('models.ForeignKey', ['Category'], {}),
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'name': ('models.CharField', [], {'max_length': '30'}),
            'publiched_date': ('models.DateField', [], {'auto_now_add': 'True'})
        },
        'books.category': {
            'id': ('models.AutoField', [], {'primary_key': 'True'}),
            'name': ('models.CharField', [], {'max_length': '20'}),
            'name2': ('models.CharField', [], {'max_length': '20'})
        },
        'authors.author': {
            '_stub': True,
            'id': ('models.AutoField', [], {'primary_key': 'True'})
        }
    }

    complete_apps = ['books']

South na końcu każdego pliku migracji zamraża obecny stan modeli dla aplikacji dla której została stworzona migracja. Umożliwia to operacje na danych z konkretnego stanu modeli.

W każdej migracji jest dostępny obiekt orm który jest nakładką na prawdziwy ORM Django, który umożliwia operacje na modelach w konkretnym stanie.

Przykład:

Jeżeli chcialibyśmy dodać migracje która np. we wszystkich obiektach typu Category w polu name zamieni znak " " na znak "-" to używając takiego zapisu:

for category in Category.objects.all():    
    category.name = category.name.replace(" ", "-")
    category.save()

To migracja ta będzie działać tylko w momencie pisania tego kodu, jeżeli w przyszłości zmienimy model Book i dopiszemy np. do niego jakieś pole to już migracja przestanie działać. Jeżeli natomiast użyjemy obiektu orm dostępnego w metodzie forwards i backwards każdej klasy migracji to będziemy mogli dokonywać operacji na modelach w konkretnym stanie.

for category in orm.Category.objects.all():    
    category.name = category.name.replace(" ", "-")
    category.save()

Teraz migracja ta będzie działać nawet jak zmieni się model Category.

Należy jednak pamiętać że south domyślenie zamraża tylko stan modeli dla aplikacji dla której tworzona jest migracja. Więc domyślenie w obiekcie orm dostępny będzie tylko model Book, Category i Author, jeżeli chcielibyśmy mieć w tej migracji dostępny także model z innej aplikacji, to musimy przy tworzeniu migracji wskazać dla jakiej aplikacji ma być dodatkowo zamrożony jej stan. Służy do tego opcja --freeze polecenia startmigration. Np. następujące polecenie zamrozi także stan aplikacji auth:

/manage.py startmigration books change_category_name --freeze=auth

Do naszego utworzonego wcześniej szablonu wpisujemy w metodzie forwards kod ktory dokona migracji danych.

class Migration:
    no_dry_run = True
    def forwards(self, orm):
        for category in orm.Category.objects.all():    
            category.name = category.name.replace(" ", "-")
            category.save() 

    def backwards(self, orm):
        for category in orm.Category.objects.all():    
            category.name = category.name.replace("- ", " ")
            category.save() 

models = {
        ...
    }

complete_apps = ['books']

Ponieważ south dla każdej migracji przed jej wykonaniem robi tzn "dry run" czyli próbuję czy uda się dokonać zmian na bazie danych, dlatego trzeba upewnić się że kod naszej migracji danych będzie wykonany tylko gdy nie jest wykonywany tryb "dry run", innaczej zmiany na danych zostną dokonane dwa razy. Można to zrobić na dwa sposoby:

  • dopisując pole: no_dry_run = True do klasy Migration
  • sprawdzając w metodzie forwards czy db.dry_run jest ustawione na False

Pisanie migracji zależnych od innych migracji

Jeżeli mamy jakąś migracje którą chcieli byśmy wykonać przed inną migracją, to do klasy migracji dopisujemy pole depends_on gdzie określamy aplikacje oraz migracje do której mają zostać wykonane wszystkie migracje przed uruchomieniem właściwej migracji. Przykład:

class Migration:

    depends_on = (
        ("authors", "0003_change_name"),
    )

    def forwards(self):
        ....

Rzeczy o których warto pamiętać przy pisaniu migracji

Należy pamiętać aby dla migracji danych także pisać migracje wycofujące zmiany w metodzie backwards. Jest bardzo ważne, z mojego doświadczenia wiem że nie pisanie migracji wstecznych się mści. W czasie tworzenia projektu, na początku mówi się że przecież wystarczy ręcznie usunąć z bazy dwie tabelki, kilka wpisów z tabeli south_migrationhistory i już można zapuścić jeszcze raz migracje, jednak gdy dochodzą migracje z różnych aplikacji to czas wykonania tej operacji się wydłuża. Dodatkowo migracje danych które wykonują się tylko w jedną stronę, mogą w przypadku błędów w kodzie powodować dodatkowe problemy. Więc lepiej zawsze pamiętać o pisaniu migracji wstecznych. Dzięki temu zawsze można wycofać wszystkie migracje i cofnąć się do dowolnego stanu bazy danych.

Bardzo ważną rzeczą jest aby nigdy nie mieszać ze sobą migracji danych i migracji struktur bazy danych. Migracje powinny być oddzielnymi częściami. Jeżeli zacznie się mieszać je ze sobą to może dojść do problemów z poprawnością wykonywania migracji. Np. jeżeli migracja składa się z dodawania kolumny do tabeli w bazie danych oraz operacji na danych to mimo iż kolumna się dopisze poprawnie do bazy danych, to i tak cała migracja może się wyłożyć przy operacji na danych, np. z powodu błędu w kodzie. Migracje powinny się wykonywać poprawnie tylko w całości.

Warto w setings.py dopisać SOUTH_TESTS_MIGRATE = False. Wtedy w trakcie uruchamiania testów będzie działać standardowe syncdb z Django. Inaczej south domyślenie podmienia syncdb z Django swoim własnym które synchronizuje tylko aplikacje dla których nie ma załączonych migracji.

Autoryzacja przez clickpass kontra rpx

RPX jest podobną usługą do clickpass - upraszcza logowanie z systemów gdzie użytkownicy już mają konta. Zasada działania jest bardzo podobna. System ten również opiera się o OpenID.

Istnieje także implementacja rpx dla django - http://code.google.com/p/django-newrpx/

rpx wydaje się lepsze od clickpass, z tego względu że użytkownik dużo łatwiej się może zalogować, praktycznie po autoryzacji w systemie zewnętrznym jest już zalogowany, przy clickpass po autoryzacji pokazuje się formularz rejestracyjny z wypełnionymi danymi( typu: email, opcjonalnie imię, nazwisko) i trzeba ręcznie wpisać nazwę użytkownika jaki będzie utworzony. W rpx się tego nie robi, wszystko się dzieje z automatu, nazwa użytkownika jest z automatu przekazywana(z systemu w którym wcześniej odbyła się autoryzacja) do backend’u autoryzacji który sam tworzy nowego użytkownika i go loguje. Przy następnym logowaniu backend sprawdza czy login użytkownika(przekazany np. z google accounts) istnieje już i go loguje. W jednym i drugim przypadku logowanie to tak naprawdę 2-3 kliknięcia myszą.

Teraz na temat samej implementacji rpx w django

Pakiet django-newrpx był dosyć z bugowany – parę drobnych bugów w kodzie, udało mi się wszystko popoprawiać i wysłać pacha do autora. W django-newrpx w przeciwieństwie do django-clickpass nie ma zaimplementowanego tzn "account merge"(samo rpx obsługuje tą opcje- Mapping API) jest to sytuacja kiedy użytkownik ma już konto na danym serwisie i chciałby skojarzyć że loguje się np. przez google accounts do tego właśnie konta. Autor django-rpx obiecuje zaimplementować także Mapping API w najbliższym czasie

Na stronie rpx pisze że teoretycznie są dostępne tłumaczenia, jednak przy próbie zmiany wersji językowej ciągle jest angielski – jednak oznacza to pewnie w końcu te tłumaczenia zaczną działać skoro opisują to w dokumentacji.

W rpx dostępne są 3 rodzaje kont, tylko podstawowe - basic jest darmowe, z zauważalnych różnic miedzy tylko kontami a clickpass jest taka że w clickpass tzn trust root jest nasza domena, a w rpx nasza domena może być trust root tylko w wersji płatnej, w wersji podstawowej trust root jest w formie domena-com.rpxnow.com. Jest to nazwa która będzie pokazywana po zalogowaniu na zewnętrznym serwisie(np. google) przy pytaniu o udostępnienie podstawowych danych do zalogowania - e-mail, nazwa użytkownika.

Wydaje mi się że mimo wszystko warto wybrać rpx, ponieważ jeszcze bardziej upraszcza autoryzacje użytkowników. Także konfiguracja samego django-rpx jest dużo prostsza niż clickpass. Wystarczą 3 wpisy w settings.py oraz jedna linia w szablonie i już gotowe - działa :) Wadą rpx jest że nie ma w wersji bezpłatnej możliwości ustawienia trust root na własną domenę.

Django - edytowalne komentarze

Domyślnie w django 1.0.2 w wbudowanych komentarzach nie ma możliwości edytowania komentarzy, brakowało mi tej funkcjonalności, więc postanowiłem rozszerzyć istniejące komentarze o edycje komentarzy. Zauważyłem także że nowe komentarze dodane w django 1.0 nie posiadają funkcji uzupełniania danych użytkownika(jeżeli jest zalogowany), podczas dodawania komentarza mimo iż model Comment jest powiązany teraz z modelem User z aplikacji auth. Rozszerzyłem więc komentarze o tą funkcjonalność. Po dyskusji na ten temat na kanale #django-pl został utworzony ticket na stronie projektu Django z tym związany.

Zależało mi aby używać tego samego mechanizmu zarówno do edycji komentarzy przez zalogowanych użytkowników jak i przy dodawaniu nowych komentarzy. Aby to zrealizować wystarczy stworzyć własnego tag'a który będzie używać klasy po dziedziczonej po klasie RenderCommentFormNode, tag'a ten będzie używany do generowania formularza dodawania lub edycji komentarza, zamiast domyślnego tag'a.

from django.contrib.comments.templatetags.comments import RenderCommentFormNode

class EditRenderCommentFormNode(RenderCommentFormNode):
    """Render the comment form directly"""

    def get_form(self, context):

        ctype, object_pk = self.get_target_ctype_pk(context)
        if object_pk:
            comment = context.get('comment', None)
            if comment:
                initial = dict(name=comment.user_name,
                        email=comment.user_email,
                        url=comment.user_url,
                        comment=comment.comment 
                        )
            else:
                request = context['request']
                initial = dict(name=request.user.get_full_name() or request.user.username,
                               email=request.user.email,
                               )

            return EditCommentForm(target_object=ctype.get_object_for_this_type(pk=object_pk), 
                                   initial=initial)
        else:
            return None

@register.tag
def render_edit_comment_form(parser, token):
    """
    Render the comment form (as returned by ``{% render_edit_comment_form %}``) through
    the ``comments/form.html`` template.

    Syntax::

        {% render_edit_comment_form for [object] %}
        {% render_edit_comment_form for [app].[model] [object_id] %}
    """
    return EditRenderCommentFormNode.handle_token(parser, token)

Teraz zamiast stosowania tag'a render_comment_form będziemy używać tag'a render_edit_comment_form

W po dziedziczonej klasie RenderCommentFormNode została nadpisana metoda get_form która teraz zwraca klasę formularza EditCommentForm. Dostaje on teraz obiekt komentarza (w przypadku edycji) oprócz standardowych parametrów przekazywanych do klasy CommentForm.

class EditCommentForm(CommentForm):
    def __init__(self, comment_instance=None, *args, **kwargs):
        self.comment_instance = comment_instance

        super(EditCommentForm, self).__init__(*args, **kwargs)

    def get_comment_object(self):
        if self.comment_instance:
            self.comment_instance.user_name = self.cleaned_data['name']
            self.comment_instance.user_email = self.cleaned_data['email']
            self.comment_instance.user_url = self.cleaned_data['url']
            self.comment_instance.comment = self.cleaned_data['comment']

            return self.comment_instance
        else:
            return super(EditCommentForm, self).get_comment_object()

Nowa klasa formularza edycji komentarzy posiada nadpisaną metodę get_comment_object która, w przypadku edycji zwraca istniejący zmieniony komentarz a w przypadku dodawania nowego komentarza zwraca nowy komentarz.

Zostało jeszcze teraz napisać widok który będzie odpowiadać za edycje komentarzy:

@login_required
def edit_comment(request, comment_id):
    comment = get_object_or_404(Comment, pk=comment_id)

    if comment.user != request.user:
        return HttpResponseForbidden('Access forbidden!!!')

    if request.POST:
        form = EditCommentForm(comment_instance=comment, target_object=comment.content_object, data=request.POST)
        if form.is_valid():
            edited_comment = form.get_comment_object()
            edited_comment.save()
            return HttpResponseRedirect(comment.get_absolute_url())

    return render_to_response('comments/edit_comment.html', 
                              {'comment':comment}, 
                              context_instance=RequestContext(request)
                              )

Widok ten należy jeszcze podpiąć w urls.py, np:

urlpatterns = patterns('', 
    (r'^your-comments/edit/(?P<comment_id>[0-9]+)/$', 'dreamblog.blog.views.edit_comment'),    
)

Na końcu musimy jeszcze stworzyć szablon: comments/form.html który będzie wykorzystywany do generowania formularza przez tag render_comment_form. Może np. wyglądać tak:

{% load i18n blog_tags %}
{% for field in form %}
{% if field.is_hidden %}
  {{ field }}
{% else %}
  <p
    {% if field.errors %} class="error"{% endif %}
    {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
    {% if field.errors %}{{ field.errors }}{% endif %}
    {{ field.label_tag }}
    {{ field }} 
    {% if field.help_text %} 
        <span id="comment_help_text">{{ field.help_text }}</span>   
    {% endif %}
  </p>
{% endif %}
{% endfor %}
<div style="margin-left: 150px;">
<input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
{% if not comment %}
<input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
{% endif %}
</div>

W django 1.1 wbudowane komentarze będą umożliwiały dużo łatwiejszy sposób na rozszerzanie ich - http://docs.djangoproject.com/en/dev/ref/contrib/comments/custom/

Jakieś pytania/sugestie ? komentarze ?

Django - autoryzacja przez Clickpass - tutorial

Clickpass umożliwia proste logowanie użytkowników za pomocą kont które już posiadają w innych systemach, np. hotmail, yahoo, google, facebook, aim, clickpass lub OpenID. Zasadę działania opisywałem w poprzednim poście. Teraz pokaże jak zaimplementować to w Django. Korzystam z aplikacji która ostatnio udostępniło washingtontimes, a mianowicie django-clickpass. Jest to aplikacja która bardzo upraszcza wykorzystanie clickpass w django.

Instalacja

Zainstaluj django-openid:

svn co http://django-openid.googlecode.com/svn/trunk/django_openid django_openid

Zainstaluj paczkę python OpenID 2.2.1 - http://openidenabled.com/files/python-openid/packages/python-openid-2.2.1.tar.gz. Szczegółowe instrukcje znajdziesz tutaj: http://www.openidenabled.com/python-openid/

Dodaj django_openid do Twoich INSTALLED_APPS.

Zainstaluj django-clickpass:

svn co http://opensource.washingtontimes.com/projects/public/django-clickpass/trunk/clickpass clickpass

Dodaj clickpass do Twoich INSTALLED_APPS.

Do głównych urls.py dodaj from clickpass.consumer import ClickPassConsumer

Dodaj (r'^openid/(.*)', ClickPassConsumer()), do Twoich urlpatterns w urls.py:

Przykład urls.py, do dodaniu ClickPassConsummer:

from clickpass.consumer import ClickPassConsumer
urlpatterns = patterns('',
    (r'^admin/(.*)', admin.site.root),
    (r'^moja_aplikacja/', include('moj_projekt.moja_aplikacja.urls')),
    (r'^openid/(.*)', ClickPassConsumer()),
)

Dodaj clickpass.consumer.ClickPassConsumer do MIDDLEWARE_CLASSES gdzieś po django.contrib.sessions.middleware.SessionMiddleware, np:

Przykład:

MIDDLEWARE_CLASSES = (
   'django.middleware.common.CommonMiddleware',
   'django.contrib.sessions.middleware.SessionMiddleware',
   'clickpass.consumer.ClickPassConsumer',
   'django_openid.consumer.SessionConsumer',
   'django.contrib.auth.middleware.AuthenticationMiddleware',
   'django.middleware.doc.XViewMiddleware',
    )

W szablonach możesz dodać przycisk clickpass za pomoca template tag'a clickpass_button_large* lub clickpass_button_small.

{% load clickpass %} 
{% if user.is_anonymous %} 
    Zaloguj się
    {% clickpass_button_small %} 
{% else %} 
    Zalogowano jako: {{user.first_name}} {{user.last_name}}
{% endif %}

Możesz także napisać kilka domyślnych szablonów które używa django-openid. Szablony są w katalogu: django_openid/templates/django_openid

Konfiguracja konta clickpass

Załóż tzw. site key, na: www.clickpass.com/developer.

  1. Krok 1 - Zarejestruj swoją domenę z nazwą oraz url'em.

  2. Krok 2 - ustaw następujące wartości:

    • OpenID trust root na http://www.twojadomena.com/
    • begin_openid_login na http://www.twojadomena.com/openid/
    • OpenID parameer label na openid_url
    • Submission method na POST
  3. Krok 3:

    • begin_add_openid_to_user ustaw na http://www.twojadomena.com/openid/add_openid_to_user/
    • user_id label ustaw na user_id
  4. Krok 4:

    • process_openid_registration_url na http://www.twojadomena.com/openid/register/
    • new_openid_completion_url zostaw puste
    • logo_url zostaw puste
    • Wymagane pola zostaw puste
  5. Dodaj następujące ustawienia do Twoich settings.py:

    • CLICKPASS_SITE_KEY = Twój site key
    • CLICKPASS_LOGO_URL = 'http://url do Twojego logo'

Uwagi

Uruchamiając django-clickpass na python'ie 2.4 miałem problem z python-openid. Jest tam bug związany z biblioteką hashlib. Aby rozwiązać problem należy nałożyć patcha dostępnego tutaj, lub uruchomić serwis na python'ie 2.5, gdzie owy błąd nie występuje.

Jeżeli chce się korzystać tylko z tej autoryzacji i używa się dekoratora login_required w widokach to należy pamiętać aby zmienić w settings.py LOGIN_URL na '/openid/'

Nowa wersja bloga - czyli jak stworzyłem bloga marzeń

Przez ostatni czas przepisywałem tego bloga na django 1.0. Gdy pisałem go około 1.5 roku temu, miałem małe pojęcie o django i python, teraz gdy nabrałem trochę doświadczenia z django i python'em, postanowiłem sobie napisać swojego bloga marzeń. Ktoś mógł by powiedzieć po co pisać bloga od podstaw, przecież można skorzystać z jakiegoś gotowca, jednak ja to traktuje jako takie poletko doświadczalne, miejsce gdzie mogę poeksperymentować z nowymi bibliotekami, narzędziami itd. Pozwala to na nauczenie się czegoś nowego, na co nie koniecznie ma się czas podczas co dziennej pracy zawodowej oraz przekazanie zdobytej wiedzy społeczności.

Mój nowy blog posiada następujące funkcje:

Autoryzacja wykorzystując django-clickpass

django-clickpass jest jednym z projektów ostatnio udostępnionych przez washingtontimes. Implementuje mechanizm autoryzacji clickpass. Clickpass pozwala na logowanie z hotmail, yahoo, google, facebook, aim, clickpass lub OpenID.

Zasada działania:

  1. Wejście na stronę
  2. Przycisk Clickpass ładuje uzupełniony formularz zawierający OpenID użytkownika
  3. Użytkownik klika w przycisk
  4. Jeżeli logujemy się pierwszy raz, clickpass zapyta czy mamy już konto w jednym z popularnych serwisów, jeżeli tak, to przekieruje do autoryzacji w tym serwisie, następnie wygeneruje OpenID i skojarzy je z serwisem który wybraliśmy.
  5. OpenID jest wysłane bezpośrednio przez Clickpass do twojej strony
  6. Następuje proces autoryzacji OpenID jak przy zwykłym OpenID

Clickpass zasada działania

Dzięki Clickpass nie jest już potrzebne podawanie URLi do konta.

Obsługa gravatarów

Zaimplementowałem obsługę gravatarów, korzystając z django-gravatar

Auto uzupełnianie formularza komentarzy

Jeżeli użytkownik jest zalogowany to w formularzu komentarza automatycznie uzupełnia się dane które przekazał logując się przez clickpass

Możliwość edycji komentarzy

Jeżeli użytkownik jest zalogowany to może edytować komentarze które dodał

Obsługa markdown w komentarzach oraz edytor wmd

Podczas dodawanie komentarzy możliwe jest korzystanie ze składni
markdown oraz dostępny jest prosty edytor(wmd) pozwalający automatycznie wstawiać składnie markdown.

Kod python'a w komentarzach

W komentarzach można wstawiać kod python'a który będzie automatycznie pokolorowany. Jeżeli napiszemy:

 [trzy dwukropki]python
#kod pythona
class MojaKlasa(object):
        """Definicja klasy"""
        pass

Dostaniemy:

    # kod pythona
    class MojaKlasa(object):
        """Definicja klasy"""
        pass

Pełny tekst w rss'ach

Przepisałem RSS'y, teraz zawierają pełny tekst wpisów.

Implementacje poszczególnych elementów opisze w kolejnych postach.

EuroDjangoCon w Pradze

Dzisiaj w internecie pojawiła się informacja że pierwsza w europie konferencja poświęcona django odbędzie się w Pradze w Czechach. Konferencja odbędzie się od 4 do 6 maja 2009 roku. Rejestracja uczestników ma zacząć się 6 lutego. Więcej informacji można znaleźć na stronie konferencji. Zapowiada się ciekawie :)

Django ma teraz szybsze testy

Do trunk'owej wersji Django został zakomitowany mechanizm testów działający na transakcjach, przyśpiesza wykonywanie testów o ok. 8 do 40 razy. Funkcjonalność ta znajdzie się w wersji Django 1.1 która będzie wydana marcu. Aby skorzystać z tej funkcjonalność należy używać silnika baz danych który obsługuje tranzakacje. Mam nadzieję że teraz już nikt nie będzie miał wymówek aby nie pisać testów :)

Zmianna hostingu

Musiałem ostatnio zmienić hosting, dreamhost już mnie wkurzył totalnie. Nagle wszystkie moje serwisy na dreamhoscie przestały działać, okazało że podobno przekroczyły limit dostępnej pamięci i ich automatyczne skrypty zaczoły mnie odcinać. Support twierdził że moje serwisy generują za duży ruch, sprawdziłem wszystko, to nie możliwe mój blog ma raptem ok 30 wejsc dziennie, potem jeszcze mam jeden serwis który ma po ok 40 wejsc, więc to nie możliwie.

Wiem że inne osoby też miały takie problemy, w ogóle ostatnio na dremhoscie padła im cała farma serwerów :)

Teraz stwierdzam że support w dremhoscie jest fatalny, gościu mi 3 razy! to samo pisał:

I've checked into why you've been receiving these internal server errors,
and it seems your scripts have been getting automatically killed by our
Process Watcher due to your sites going over Memory limits on the shared
server:
[Wed Oct 29 09:31:55 2008] [error] [client 66.249.70.129] Premature end
of script headers: dispatch.fcgi


I would highly recommend that you follow the steps in the following wiki
article in order to reduce your usage:

http://wiki.dreamhost.com/Finding_Causes_of_Heavy_Usage

Pokazywałem że to nie prawda że wszytko sprawdziłem, a on to samo, tak jakby nie czytał co pisze. Miałem wrażenie że tak jak w TPSA próbują mi wmówić że to moja wina :) Ja mu pisze wykonałem wszystkie kroki na wiki, a on dalej swoje, już miałem wrażenie że to może jakiś automat :)

Wkurzyłem się i przeniosłem hosting na megiteam.pl, na razie jestem zadowolony, wszystko bardzo fajnie działa. Spodobał mi sie ten panel na megiteam.pl, bardzo przyjemnie się konfiguruje wszytko.

Rozważałem też hosting który kiedyś opisywał Jarek Zgoda na swoim blogu: djangohosting.cz, hosting tylko pod django, jednak na razie dałem szanse megiteam.pl. Może kiedyś dam też szanse temu drugiemu.

django.pl wystartowało

Dzisiaj ok godziny 12 uruchomiliśmy serwis django.pl. Głównym celem serwisu jest szeroko pojęta promocja Django oraz Python w Polsce. Dziękuję wszystkim którzy przez ostanie dni zmobilizowali się ciężko pracowali aby uruchomić serwis. Chyba wpłynoł na to pobyt na konferecji PyCon PL, wprawdzie było nas tam z django.pl (ludzi najbardziej aktywnych i bardzo zaangarzowanych) tylko troje, ale to wystarczyło, nakręciliśmy sie nawzajem, ta fala chyba udzieliła sie również innym. Mnie bardzo na konferecji zmotywował Marcin Mierzejewski na swojej prezetacji o django. Na koniec Marcin powiedział, że chciał dać do linków na temat django django.pl ale jeszcze nie działa, tym mnie bardzo zmotywował. Teraz Marcin na następiej prezentacji smiało możesz dać link django.pl, juz działa :))

Django Databrowse - rejestracja modeli z wszystkich aplikacji

Jakiś czas temu zainteresowałem się aplikacją dostarczaną razem z Django - databrowse. Aplikacja umożliwia przeglądanie wszystkich danych  z  bazy danych.  Należy jednak podać dla jakich modeli ma pokazać dane. Aby uaktywnić databrowse należy dodać do url'i adres pod którym będzie dostępne. Np:

    (r'^databrowse/(.*)', databrowse.site.root),

Należy także wskazać jakie modele będa dostępne, w następujący sposób:

    databrowse.site.register(NazwaModelu)

Sposób ten jest jednak uciążliwy ponieważ jeżeli by chciało się dodać do databrowse wszystkie modele z wszystkich aplikacji które mamy zainstalowane w settings.py to trzeba by ręcznie je wpisać wszystkie.

Najlepsze rozwiązanie to dodać wszystkie modele z wszystkich aplikacji z automatu. Miałem jednak problem jak automatycznie pobrać nazwy wszystkich modeli z danej aplikacji. Jednak po zastanowieniu doszło do mnie że przecież przy wykonaniu manage.py syncdb robione jest dokładnie to co potrzebuje.  Przejrzałem kod źródłowy Django który wykonuje syncdb  i odkryłem że po zaimportowaniu z django.db modułu models, mamy dostępną funkcje: get_apps() która zwróci nazwy wszystkich zainstalowanych apliakacji. Następnie funkcja get_models(nazwa_aplikacji) zwróci nam klasy wszystkich modeli dla danej aplikacji. Na tym etapie można już w bardzo prosty sposób zarejestrować wszystkie modele do databrowse:

from django.db import models
for app in models.get_apps():
    model_list = models.get_models(app)
    for model in model_list:
        databrowse.site.register(model)

Grupa Django PL

Została założona grupa django-pl. Teraz djangonauci nie gęsi i również swoją grupę maja :)

Myślę że w najbliższych dniach uruchomimy django.pl na kodzie z djangoproject. Jeżeli ktokolwiek chciałby pomóc w tworzeniu społeczności django w Polsce to zapraszam na grupę django-pl.

Tutoriale wideo - Django

Michael Trier prezentuje wideo tutorial'e pokazujące różne właściwości Django. Do to tej pory opublikował pięć tutoriali, co jakiś czas dodaje kolejne:

  • Epizod 001 - pokazuję implementacje prostych kanałów RSS do aplikacji blogowej
  • Epizod 002 - pokazuję implementacje kanałów RSS dla konkretnych elementów na aplikacji blogowej
  • Epizod 003 - pokazuje jak dodawać polecenia użytkownika do narzędzia manage.py Django
  • Epizod 004 - pokazuje jak zaimplementować spersonalizowane formularze z newforms
  • Epizod 005 - mapowanie url'i za pomocą mechanizmu flatpage

Jak znaleźć ludzi zajmujących sie Django

Simon Willison oraz Natalie Downe uruchomili serwis dedykowany dla developerów Django djangopeople.net. Serwis powstał ponieważ często trudno odnaleźć ludzi zajmujących Django, jest to skutek dobrej dokumentacji Django, która sprawia że ludzie po prostu czytają dokumentacje i często w ogóle nie zadają pytań na listach dyskusyjnych, ani w żaden inny sposób nie dają o sobie znać. Stąd pomysł utworzenia takiego serwisu, pozwalającego odnaleźć developerów z całego świata.

W 24 godziny po uruchomieniu serwisu zarejestrowało się ponad 750 ludzi z całego świata, liczba ta stale rośnie. Do serwisu można zalogować się także za pomocą OpenID, uważam że to super pomysł, dzięki temu konto miałem aktywne w ciągu minuty!!!

Każdy użytkownik w swoim profilu może podać swój opis, miejsce gdzie się znajduje (za pomocą mapy google), jakie ma umiejętności oraz w jakich projektach uczestniczył. W serwisie można wyszukiwać ludzi po kraju, mieście, umiejętnościach. Można zobaczyć gdzie najbliżej Ciebie są ludzie zajmujący się Django. Jest mapa na której można zobaczyć wszystkich ludzi zarejestrowanych w djangopeople.net. Twórcy serwisu chcą w przyszłości dodać funkcjonalność umożliwiającą łatwiejsze organizowanie spotkań lokalnych grup developerów Django. Mają także nadzieje że serwis przyczyni się do organizowania lokalnych grup djangonaut'ów.

Jeżeli zajmujesz się Django zawodowo lub jako hobby to nie ukrywaj się i zaloguj się na djangopeople.net

Djangobook została wydana

Książka pt. The Definitive Guide to Django: Web Development Done Right napisana przed głównych developerów Django - Adriana Holovaty oraz Jacob'a Kaplan-Moss'a została wydana przez wydawnictwo Appress. Na głównej stronie wydawnictwa jest informacja że książka jest juz w sprzedaży. Jest tam także przykładowy rozdział oraz spis treści. Zamówiłem dziś swoja kopie na amazon.com, czekam z niecierpliwością kiedy do mnie dojdzie :) Wersja beta książki jest dostępna za darmo na stronie http://www.djangobook.com

Profilownie kodu w Pythonie oraz Django

Ostatnio chciałem sprofilować moją aplikacje w Django. Postanowieniem przyjrzeć się tej kwestii. W Pythonie od wersji 2.2 w bibliotece standardowej jest profiler kodu HotShot(High performance logging profiler) Jest on zamiennikiem istniejącego we wcześniejszych wersjach Pythona modułu Profile. Moduł HotShot jest interface'em do modułu _hotshot z C, dzięki temu temu jest dużo szybszy niż stary moduł Profile i daje bardziej precyzyjne wyniki.

Przykład profilowanie dowolnego kodu z pomocą modułu HotShot:

Profilowanie funkcji moja_funkcja:

import hotshot

def moja_funkcja():
    j = 0
    for i in range(1,10000):
        j += i
       
    return j

if __name__ == '__main__':
    prof = hotshot.Profile("mojprofil.prof")
    prof.run("moja_funkcja()")
    prof.close()

Metoda Profile stworzy plik w którym zostaną zapisane statystki profilowania. Można także pomiędzy dowolny kod wstawić gdzie ma zacząć sie profilowanie a gdzie skończyć:

import hotshot

prof = hotshot.Profile("mojprofil.prof")
prof.start()

# ...tutaj kod który ma zostać sprofilowany...

prof.stop()
prof.close()

Drukowanie wyników:

import hotshot.stats

stats = hotshot.stats.load("mojprofil.prof")
stats.strip_dirs()
stats.sort_stats('time', 'calls')
stats.print_stats(20)
2 function calls in 0.004 CPU seconds

   Ordered by: internal time, call count

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.004    0.004    0.004    0.004 profile_test.py:3(moja_funkcja)
        1    0.000    0.000    0.004    0.004 :1()
        0    0.000             0.000          profile:0(profiler)

W zwróconym wyniku mamy następujące parametry:

ncalls
Ilość wywołań funkcji/metody
tottime
Całkowity czas wykonań danej funkcji wykluczając inne pod funkcje.
percall
Czas wykonania pojedynczej funkcji.
cumtime
Całkowity czas wykonania wszystkich funkcji o tej samej nazwie łącznie z innymi pod funkcjami które wywołuje ta funkcja.
percall
Czas wykonania pojedynczej funkcji, wraz z pod funkcjami, które wywołuje ta funkcja.
filename:lineno(function)
nazwa pliku:nr_linii(funkcja)

Tutaj można znaleźć dekorator który umożliwia, oznaczenie danej funkcji czy metody aby była sprofilowana

Wygenerowane wyniki można także zobaczyć w postaci graficznej, potrzebny jest do tego program kcachegrind dostępny pod linux'em. Nie znalazłem jakiegoś odpowiednika pod Windows'a, podobno można go uruchomić pod Windows'em z pomocą Cygwina, ale nie sprawdzałem bo używam Ubuntu, zarówno w pracy jak i w domu. Aby zobaczyć graficzne wyniki profilowania trzeba je skonwertować do formatu obsługiwanego przez kcachegrind. Potrzebny do tego jest pakiet kcachegrind-converters. Po zainstalowaniu pakietu wykonujemy polecenie hotshot2calltree nasz_plik_z_profilem > cachegrind.out.01.

Jeżeli chodzi profilowanie Django to istnieje kilka możliwości. Django posiada hander django.core.handlers.profiler-hotshot który umożliwia profilowanie widoków gdy używa się mod_pythona, druga możliwość to uruchomienie django przez serwer WSGI. Dokładny opis można znaleźć tutaj. Najbardziej ciekawy i elegancki sposób jaki znalazłem to profilowanie django za pomocą middleware'a dostępnego tutaj. Wystarczy dopisać go na początek listy middlewre'ów naszej aplikacji w settings.py. Aby z profilować dowolny widok wystarczy dopisać na koniec url'a ?prof. Np. http://mojadomena.com/mojwidok/?prof. Po wywołaniu takiego widoku na ekranie otrzymamy statystyki profilowania.

Django ComplexField i ComplexMultiWidget

Ostatnio przeglądając kod źródłowy Django, a mianowicie testy, natknąłem się na pole MultiValueField oraz widget - MultiWidget. Jak na razie nie zostało to udokumentowane w dokumentacji newforms. Można dzięki temu stworzyć sobie pole które będzie się składać z innych pól. Poniżej przykład:

Definicja klasy ComplexMultiWidget, która zawierać będzie widget'y pól składowych klasy ComplexField:

class ComplexMultiWidget(forms.MultiWidget):
    def __init__(self, attrs=None):
        widgets = (
            forms.TextInput(),
            forms.SelectMultiple(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))),
            forms.SplitDateTimeWidget(),
        )
        super(ComplexMultiWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            data = value.split(',')
            return [data[0], data[1], datetime.datetime(*time.strptime(data[2], "%Y-%m-%d %H:%M:%S")[0:6])]
        return [None, None, None]
    def format_output(self, rendered_widgets):
        return u'\n'.join(rendered_widgets)

Definicja klasy pola ComplexField. W metodzie compress zapisujemy w jaki sposób wartości z pól składowych mają być zapisane w naszym polu typu ComplexField.

class ComplexField(forms.MultiValueField):
    def __init__(self, required=True, widget=None, label=None, initial=None):
        fields = (
            forms.CharField(),
            forms.MultipleChoiceField(choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))),
            forms.SplitDateTimeField()
        )
        super(ComplexField, self).__init__(fields, required, widget, label, initial)

    def compress(self, data_list):
        if data_list:
            return '%s,%s,%s' % (data_list[0],''.join(data_list[1]),data_list[2])
        return None

Teraz tworząc instancje pola ComplexField, podająć jako widget nasz ComplexMultiWidget i wywołując metode clean dla poprawnych i nie poprawnych danych dostaniemy:

>>> f = ComplexField(widget=ComplexMultiWidget())
>>> f.clean(['some text', ['J','P'], ['2007-04-25','6:24:00']])
u'some text,JP,2007-04-25 06:24:00'
>>> f.clean(['some text',['X'], ['2007-04-25','6:24:00']])
Traceback (most recent call last):
...
ValidationError: [u'Select a valid choice. X is not one of the available choices.']

>>> f.clean(['some text',['JP']])
Traceback (most recent call last):

Aby wykorzystać nasze nowe pole tworzymy klasę formularza i dodaje definicje pola:

>>> class ComplexFieldForm(Form):
            field1 = ComplexField(widget=ComplexMultiWidget())

Po wydrukowaniu formularza dostaniemy:

>>> f = ComplexFieldForm()
>>> print f
<tr><th><label for="id_field1_0">Field1:</label></th><td><input type="text" name="field1_0" id="id_field1_0" />
<select multiple="multiple" name="field1_1" id="id_field1_1">
<option value="J">John</option>
<option value="P">Paul</option>
<option value="G">George</option>
<option value="R">Ringo</option>
</select>
<input type="text" name="field1_2_0" id="id_field1_2_0" /><input type="text" name="field1_2_1" id="id_field1_2_1" /></td></tr>

Formularz z wypełnionymi danymi:

>>> f = ComplexFieldForm({'field1_0':'some text','field1_1':['J','P'], 'field1_2_0':'2007-04-25', 'field1_2_1':'06:24:00'})
>>> print f
<tr><th><label for="id_field1_0">Field1:</label></th><td><input type="text" name="field1_0" value="some text" id="id_field1_0" />
<select multiple="multiple" name="field1_1" id="id_field1_1">
<option value="J" selected="selected">John</option>
<option value="P" selected="selected">Paul</option>
<option value="G">George</option>
<option value="R">Ringo</option>
</select>
<input type="text" name="field1_2_0" value="2007-04-25" id="id_field1_2_0" /><input type="text" name="field1_2_1" value="06:24:00" id="id_field1_2_1" /></td></tr>

>>> f.cleaned_data
{'field1': u'some text,JP,2007-04-25 06:24:00'}

Dzięki takiemu polu MultiValueField w połaczeniu z MultiWidget możemy tworzyc skąplikowane pola, zawierające inne pola.

Advanced Django zaawansowana prezentacja

Na stronie http://www.slideshare.net/simon/advanced-django/ Można znaleźć prezentacje która odbyła sie podczas tegorocznego PyCon'u. Prezentacje prowadził jeden z developerów Django - Simon Willson. Prezentacja omawia takie tematy jak:

  • Unit testing
  • Tworzenie formularzy z Newforms
  • Wykorzystanie Ajax'a
  • implementacja OpenID w Django

Bardzo ciekawa jest sekcja na temat Ajaxa w Django. Ciekawy jest komentarz autora na temat obslugi Ajax'a w Django. "Django często stoi niżej w porównaniach frameworków ze względu na brak wbudowanej obsługi Ajaxa z pudełka". Zgadzam sie całkowicie z autorem że Django bez konkretnej bibloteki Ajaxowej to zaleta a nie wada. Ponieważ umożliwia to programiście na użycie z Django dowolnej bibloteki Ajaxowej, takiej jaka mu odpowiada. Myśle że gdy Django posiadało konkretna bibloteke Ajaxowa to było by to ograniczenie. Poza tym było by to niezgodne z filozofia samego Django. Myśle że jest to zaleta, dzieki temu Django ma obsluge dowolnego formatu Ajax'owego: fragmenty html'a, XML, JSON. Tylko od programisty zależy jakiego framework'a Ajaxowego i jakiego formatu użyje, daje to dużą elastyczność.

O mnie

Nazywam się Dominik Szopa. Jestem programistą, entuzjastą języka Python oraz rozwiązań na nim opartych. W Pythonie programuje zarówno w pracy jak i w czasie wolnym. Język ten przypadł mi bardzo do gustu ponieważ jestem osobą która zawsze wszytko musi mieć robione perfekcyjnie, dzięki Pythonowi, programowanie stało się dla mnie czystą przyjemnością. Urzekła mnie prostota rozwiązań oraz czytelność kodu.

Pamiętam jak po raz pierwszy zetknąłem się z Pythonem w pracy. Miałem napisać prosty skrypt pokazujący wykorzystanie naszego API za pomocą protokołu SOAP. Wtedy Python był dla mnie językiem "egzotycznym", pierwszy raz wtedy usłyszałem tą nazwę, było to jakieś 1,5 roku temu. Najbardziej mnie zaskoczyło, jak się zapytałem kumpla jak oddzielać bloki kodu, zaskoczyła mnie odpowiedz "za pomocą wcięć". Wtedy wydało mi się to dziwne, ale po chwili zastanowienia stwierdziłem że to przecież genialne, wymusza na programiście, porządne pisanie kodu. Tak samo po co te wszystkie nawiasy klamrowe i średniki na końcu linii w innych językach, przecież i tak robie wcięcia to po co mi jeszcze to. Na studiach zaczynałem od takich języków jak: Pascal, potem Object Pascal(Delphi), C++, Java, PHP, nawet liznąłem trochę .NET'u.

Jednak żaden z tych języków nie dawał mi czystej przyjemności z programowania, pisałem w tym ponieważ albo musiałem(studia) albo nie znałem nic lepszego. Dopiero gdy odkryłem Pythona i mogłem w nim pisać zawodowo, programowanie stało się dla mnie czystą przyjemnością i pasją. Od chwili gdy zacząłem programować w Pythonie, wiem że w tym języku chce się specjalizować i w żadnym innym.

Około rok temu ze strony showmedo.com dowiedziałem się o framework'u do tworzenia aplikacji webowych Turbo Gears, zaciekawiło mnie to że jest coś takiego co ułatwia pisanie aplikacji webowych, jednak Turbo Gears nie przypadło mi do gustu z racji tego że było mieszanka innych narzędzi, framework'ów, na sieci było mnóstwo nie przychylnych opinii na ten temat. Potem dowiedziałem się o framework'u Django.

Przerobiłem tutorial i po prostu mnie oczarowało, urzekła mnie na ta prostota i łatwość z jaką się pisze aplikacje webowe w Django. Automatyczny panel administracyjny, ORM zamiast klepania sql'a, ładne mapowanie url'i, generowanie formularzy, wiele by tu można wymieniać. Pamiętam że w PHP napisanie jakieś większej aplikacji czy nawet takiego bloga jak ten z CMS'em to była porostu rzeźnia, już mnie mówiąc o samym PHP w porównaniu do Pythona, ale to już inna bajka. Jak zobaczyłem jak prosto w Django się pisze takie aplikacje webowe to stwierdziłem, to jest to czego mi było trzeba. Spodobało mi się profesjonalne podejście developerów Django, gdy dowiedziałem się że wszystko co jest w Django zostało napisane od zera. Jeszcze bardziej spodobało mi się to że Django jest napisane w Pythonie, wiec niejako "dziedziczy" zalety samego Pythona :)

Chciałbym tutaj pisać o różnych problemach i ciekawostkach z którymi spotykam się pisząc w Pythonie. Uważam że wiedza to dobro wspólne i powinno się nią dzielić.