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