Społecznościówka w 3 krokach - krok 3

Co już mamy

W poprzednich dwóch wpisach stworzyliśmy sobie model użytkownika, umożliwiliśmy jego rejestrację oraz uwierzytelnienie. Dzisiaj rozszerzymy ten model o stosunek przyjaźni, a obecność uwierzytelnionego użytkownika wykorzystamy, aby kontrolować dostęp do strony.

Tworzymy relację

Przede wszystkim potrzebne nam jest opisanie przyjaźni jako relacji w bazie danych. W tym celu dodajemy tablicę asocjacyjną:

friendships_table = sa.Table(
    'friendships', metadata,
    sa.Column(
        'requester_id', sa.types.Integer(), sa.ForeignKey('users.id'),
        primary_key = True
    ),
    sa.Column('acceptor_id', sa.types.Integer(), sa.ForeignKey('users.id'),
              primary_key = True
             ),
    sa.Column('accepted', sa.types.Boolean()),
)

Oprócz normalnych dla tego typu tablicy kluczy odnoszących się do użytkownika (razem klucze te są kluczem głównym tej tabeli) umieszczamy jeszcze flagę, dzięki której tablica ta będzie służyć nam zarówno do przechowywania zaproszeń do przyjaźni, jak i przyjaźni zaakceptowanych. Pozostaje nam zamapować to do ORM dodając klasę Friendship i modyfikując mapowanie klasy User:

class Friendship(object):
    def __init__(self, requester_id, acceptor_id):
        self.requester_id = requester_id
        self.acceptor_id = acceptor_id
        self.accepted = False

orm.mapper(User, users_table, properties = {
    'friends': orm.relationship(
        User, secondary = friendships_table,
        primaryjoin = (
            (friendships_table.c.requester_id == users_table.c.id) &
            (friendships_table.c.accepted == True)
        ), secondaryjoin = (
            friendships_table.c.acceptor_id == users_table.c.id
        ), backref = 'back_friends')
})

orm.mapper(Friendship, friendships_table, properties = {
    'requester': orm.relationship(User, primaryjoin = (
        (friendships_table.c.requester_id == users_table.c.id) &
        (friendships_table.c.accepted == False)
    )),
    'acceptor': orm.relationship(User, primaryjoin = (
        (friendships_table.c.acceptor_id == users_table.c.id) &
        (friendships_table.c.accepted == False)
    ))
})

Ponieważ sqlalchemy nie posiada jeszcze ładnego wsparcia dla relacji symetrycznych, mamy dwie własności friends i back_friends, które razem dają wszystkich przyjaciół użytkownika. Musimy o tym pamiętać tworząc predykat.

Predykat przyjaźni

Autoryzacja repoze.what opiera się na tzw. predykatach, czyli warunkach, jakie spełnia, bądź nie spełnia uwierzytelniony użytkownik. Wbudowane predykaty odnoszą się do przynależności do grup bądź posiadania uprawnień. My jednak stworzymy własny predykat, który przetestuje, czy użytkownik jest przyjacielem danego użytkownika. Kod umieścimy w pliku social/lib/predicates.py

from repoze.what.predicates import Predicate
from social.model import Session, User
from sqlalchemy.orm.exc import NoResultFound

class is_friend(Predicate):
    message = "User must be a friend to do it"
    def __init__(self, username, *args, **kwargs):
        self.username = username
        super(is_friend, self).__init__(*args, **kwargs)

    def evaluate(self, environ, credentials):
        print credentials, self.username
        if credentials['repoze.what.userid'] == self.username:
            return # It's always ok to view your own data
        try:
            user = Session.query(User).filter(
                User.login == credentials['repoze.what.userid']
            ).one()
        except NoResultFound:
            self.unmet()

        if self.username in [u.login for u in user.friends]:
            return

        if self.username in [u.login for u in user.back_friends]:
            return

        self.unmet()

Predykat is_friend jest spełniony wtedy i tylko wtedy, jeśli testujemy go dla użytkownika zalogowanego, albo jego przyjaciela (sprawdzamy zarówno friends jak i back_friends).

Czego będziemy bronić?

Do naszego kontrolera dodajmy kolejną akcję:

def display(self, login):
    try:
        user = Session.query(User).filter(User.login == login).one()
    except NoResultFound:
        abort(404)

    c.login = user.login

    if is_friend(user.login).is_met(request.environ):
        return render('/users/display_full.mako')
    else:
        return render('users/suggest_friend.mako')

Akcja ta wyświetla "Tablicę użytkownika", lub też, jeśli nie ma prawa do jej wyświetlenia - sugestię zaprzyjaźnienia się. Szablony obu widoków na razie będą bardzo proste. Na tablicy wyświetlimy tylko login użytkownika:

<%inherit file="/base.mako" />
<h1>Tablica użytkownika ${c.login}</h1>

A jako sugestię - formularz:

# vim set fileencoding=utf-8
<%inherit file="/base.mako" />
Aby zobaczyć tablicę tego użytkownika musisz zostać jego przyjacielem:
<form action="${url(controller = 'users', action = 'befriend', login=c.login)}" method="POST">
<input type="submit" text="Zaprzyjaźnij się"/>
</form>

Formularz ten wysyła puste zapytanie do akcji befriend, którą teraz napiszemy:

from pylons.decorators.rest import restrict

@restrict('POST')
def befriend(self, login):
    try:
        requester = Session.query(User).filter(
            User.login == request.environ['REMOTE_USER']
        ).one()
    except (NoResultFound, KeyError):
        abort(401)

    try:
        acceptor = Session.query(User).filter(
            User.login == login
        ).one()
    except NoResultFound:
        abort(404)

    # Check if this side already requested friendship
    try:
        existing = Session.query(Friendship).filter(
            (Friendship.requester_id == requester.id) &
            (Friendship.acceptor_id == acceptor.id)
        ).one()
    except NoResultFound:
        pass
    else:
        if existing.accepted:
            c.message = u'Już jesteś przyjacielem tego użytkownika'
        else:
            c.message = u'Już wysłałeś zaproszenie do tego użytkownika. Poczekaj na akceptację'

        return render('users/request_done.mako')

    # Check if the other side already requested friendship
    try:
        existing = Session.query(Friendship).filter(
            (Friendship.requester_id == acceptor.id) &
            (Friendship.acceptor_id == requester.id)
        ).one()
    except NoResultFound:
        friendship = Friendship(
            requester_id = requester.id,
            acceptor_id = acceptor.id,
        )
        Session.add(friendship)
        Session.commit()
        c.message = u'Wysłano zaproszenie do użytkownika %s.' % acceptor.login
    else:
        if existing.accepted:
            c.message = u'Już jesteś przyjacielem tego użytkownika'
        else:
            existing.accepted = True
            Session.commit()
            c.message = u'Zostałeś przyjacielem użytkownika %s.' % acceptor.login
    return render('users/request_done.mako')

Używamy metody POST zgodnie z zaleceniami HTTP, które mówią, że wszystkie akcje zmieniające jakoś stan aplikacji nie powinny iść metodą GET. W akcji testujemy wszystkie możliwe scenariusze - zarówno ponowne próby, jak i sytuację, gdy druga strona już wysłała zaproszenie. Funkcja choć długa, jest dosyć prosta i łatwo można ją przeanalizować. Wszystkie akcje renderują ten sam szablon, który wyświetla tylko zmienną message z kontekstu:

<%inherit file="/base.mako" />
${c.message}

I tutaj koniec... wprowadzenia

I tutaj nasz tutorial dobiega końca. Pewnie u wielu osób pozostawia to pewien niedosyt, na razie mamy tylko możliwość zaprzyjaźnienia się dwóch użytkowników, gdy obaj sami z siebie tego zechcą, jednak dalszą pracę pozostawiam Wam. Nie tylko z wrodzonego lenistwa. Tutorial omówił bowiem wszyskti istotne mechanizmy, które pozwolą Wam dodawać do aplikacji nowe funkcjonalności. Moje propozycje dalszej zabawy:

  1. Zapoznanie się z dekoratorami z modułu repoze.what.pylonshq, które są najprostrzym rozwiązaniem dla autoryzacji w aplikacjach pylons
  1. Przy pomocy powyższych dodanie akcji umożliwiającej prowadzenie mikrobloga na swojej tablicy i czytanie go przez przyjaciół.
  2. Rozsądniejsze zarządzanie prośbami o zaprzyjaźnienie.

Meta

Published: Mar 2, 2011

Author: zefciu

Comments:  

Word Count: 1,466

Next: Wyginanie pythona - kontener dyskowy

Previous: Społecznościówka w 3 krokach - krok 2 - uwierzytelnianie

Bookmark and Share

Tags

autoryzacja repoze

Follows Up On

Article Links

  1. Built-in predicate checkers — repoze.what v1.0.9 documentation
Comments powered by Disqus