update oraz delete

8 stycznia 2017 15:43
Artur Grochowski
python development

Niestety okres świąteczny opóźnił pojawienie się artykułu, za co przepraszam.

AKTUALNY STAN APLIKACJI

Po poprzedniej części wykładu powinniśmy być już w stanie w dowolny sposób filtrować naszą listę notatek (implementacja metod filter oraz get). Co jest nam jeszcze potrzebne do lepszego komunikowania się z naszą abstrakcyjną bazą danych to operacje edycji oraz usuwania notatek.

Posiadamy więc mniej więcej taki kod (można skorzystać z podanego):

# coding: utf-8
from __future__ import unicode_literals
from shutil import copyfile
import json
import os

DATABASE = 'notes_data/notes.json'
BOARDS = ['to do', 'in progress', 'done']


class NotesManagerMixin(object):

    def count(self):
        return len(self.notes)

    def filter(self, *args, **kwargs):
        result = self.notes
        for key, value in kwargs.iteritems():
            result = [
                note for note in result
                if getattr(note, key, None) == value
            ]
        return NotesQueryset(result)

    def get(self, *args, **kwargs):
        notes = self.filter(**kwargs)
        if notes.count() == 1:
            return notes[0]
        else:
            raise IndexError('Note doesn\'t exist or returned more then one entry')


class NotesQueryset(NotesManagerMixin):

    def __init__(self, notes):
        self.notes = [note for note in notes]

    def update(self, *args, **kwargs):
        # to do
        pass

    def delete(self):
        # to do
        pass

    def __getitem__(self, idx):
        return self.notes[idx]

    def __str__(self):
        return str(self.notes)

    def __repr__(self):
        return self.__str__()


class NotesManager(NotesManagerMixin):

    def __init__(self):
        self.notes = []

    def __iter__(self):
        return self.next()

    def __generate_id(self):
        try:
            return max(note.id for note in self.notes) + 1
        except ValueError:
            return 1

    def all(self):
        return NotesQueryset(self.notes)

    def add(self, idx, board, message):
        self.notes.append(Note(idx=idx, board=board, message=message))

    def create(self, board, message):
        note = Note(
            idx=self.__generate_id(),
            board=board,
            message=message
        )
        note.clean()

        self.notes.append(note)
        note.save()

        return note

    def next(self):
        for note in self.notes:
            yield note

    def to_dict(self):
        return [note.to_dict() for note in self.notes]


class Note(object):
    objects = NotesManager()

    def __init__(self, idx, board, message):
        self.id = idx
        self.board = board
        self.message = message

    def __str__(self):
        return 'ID: {}, Board: {}, Message: {}'.format(
            self.id,
            self.board,
            self.message
        )

    def __repr__(self):
        return self.__str__()

    def clean(self):
        if not self.message:
            raise ValueError('Message is required')

        if self.board not in BOARDS:
            raise ValueError('Board "{}" doesn\'t exists'.format(self.board))

        if type(self.id) != int:
            raise ValueError('Note id "{}" is invalid'.format(self.id))

    def save(self):
        for key, note in enumerate(self.objects):
            if note.id == self.id:
                self.objects.notes[key] = self
                break

        with open(DATABASE, 'w') as database_file:
            json.dump(self.objects.to_dict(), database_file, indent=4)

        return True

    def delete(self):
        # to do
        pass

    def to_dict(self):
        return {
            'id': self.id,
            'message': self.message,
            'board': self.board
        }


def initialize():
    try:
        os.remove(DATABASE)
    except OSError:
        pass

    notes_backup_path = os.path.join('notes_data', 'notes_backup.json')
    copyfile(notes_backup_path, DATABASE)


def load_initial_data():
    initialize()

    with open(DATABASE, 'r') as database_file:
        json_data = json.load(database_file, encoding='utf-8')

    for item in json_data:
        Note.objects.add(
            idx=item['id'],
            board=item['board'],
            message=item['message'],
        )


load_initial_data()

W powyższym kodzie pojawia się kilka nowych rzeczy, które nie były wymagane do zrealizowania filtrowania, ale znacznie ułatwiają pracę z kodem, więc warto się z nimi zapoznać.

Po pierwsze zostało tutaj wykorzystane dziedziczenie. Polega ono na przekazaniu metod i atrybutów klasy bazowej wszystkim klasom dziedziczącym. Syntax w pythonie prezentuje się w następujący sposób:

class NotesQueryset(NotesManagerMixin):
    ...

Oznacza to, że klasa NotesQueryset dziedziczy wszystkie metody (count, filter i get) oraz atrybuty (brak) klasy NotesManagerMixin. Python umożliwia również wielodziedziczenie (zgodnie z algorytmem linearyzacji C3). Po co dziedziczyć? Żeby nie pisać dwa razy tego samego kodu (w tym przykładzie implementacje tych metod w obu klasach byłyby identyczne) + umożliwia to łatwiejsze zmiany w przyszłości (bo wystarczy wtedy zmienić kod tylko w jednym miejscu).

Pojawia się również tutaj metoda __iter__ która sprawia, ze obiekty tej klasy będą iterowalne (definiujemy w tej metodzie zachowanie podczas iterowania w pętli for). W klasie NotesQueryset zastosowana została metoda __getitem__ - nadpisuje ona zachowanie operatora [...], oraz również sprawia, że obiekty będą iterowalne.

Wróćmy jednak do naszego celu, czyli implementacji metod update oraz delete

UPDATE

Załóżmy, że wywołanie:

Note.objects.filter(message='test update')

zwróci nam 3 następujące notatki:

[ID: 11, Board: to do, Message: test update, ID: 12, Board: to do, Message: test update, ID: 13, Board: to do, Message: test update]

w takim wypadku po ich edycji nasza nową metodą update:

Note.objects.filter(message='test update').update(board='in progress')

chcemy aby nasze wpisy zostały zedytowane zarówno w naszym programie, zapisane do naszej bazy (plik json) i zwrócone w formie NotesQueryset (aby można było dalej coś z nimi zrobić). Czyli po wywołaniu powyższego kodu w konsoli powinniśmy uzyskać:

[ID: 11, Board: in progress, Message: test update, ID: 12, Board: in progress, Message: test update, ID: 13, Board: in progress, Message: test update]

Przykładowa implementacja:

    def update(self, *args, **kwargs):
        for note in self.notes:
            for key, value in kwargs.iteritems():
                setattr(note, key, value)
            note.save()
        return self

Pojawia się tutaj nowa wbudowana funkcja pythonowa setattr. Nie robi ona nic innego jak zmiana atrybutu (przekazanego na drugiej pozycji jako string) danego obiektu na podaną wartość (trzeci argument).

DELETE

Operacja delete powinna nam umożliwiać usuwanie zarówno pojedynczych notatek jak i całych zbiorów (NotesQueryset). Oznacza to, że wywołanie:

Note.objects.filter(message='test update').update(board='in progress').delete()

musi być możliwe i powinno kolejno znaleźć nam notatki, które mają message równy 'test update', zaktualizować ich message do 'in progress' i następnie je usunąć.

Powinno być też możliwe wywołanie (zakładając, że istnieje taka notatka):

Note.objects.filter(message='test update')[0].delete()

dlatego mamy metodę delete zarówno na klasie NotesQueryset jak i Note

Bardzo łatwo rozwiązać problem implementacji delete w klasie NotesQueryset zakładając, że mamy już ją w klasie Note (przeiterowanie przez wszystkie notatki i wywołanie kolejno .delete() dla każdej):

    def delete(self):
        for note in self.notes:
            note.delete()
        return self

Przejdźmy więc do tej drugiej:

    def delete(self):
        result = None

        for row, note in enumerate(self.objects):
            if note.id == self.id:
                result = self.objects.notes.pop(row)
                break

        with open(DATABASE, 'w') as database_file:
            json.dump(self.objects.to_dict(), database_file, indent=4)

        return result

Jak widać musimy wyszukać notatkę na naszej liście po id (a bardziej index w którym się znajduje - oczywiście w normalnych bazach danych jest to inaczej zaimplementowane), usunąć ją z listy (metoda .pop), zapisać aktualny stan notatek do pliku i zwrócić usuniętą notatkę.

Tym sposobem udało nam się napisać prostą imitację interfejsu bazy danych :) (nie jest perfekcyjna, ale wystarczająca do naszych potrzeb).

W następnych częściach kursu zajmiemy się już ciekawszą częścią web developmentu, bo zaczniemy zabawę z frameworkiem Django.

Kontakt

W razie pytań/sugestii można kontaktować się na adresy:

  • artur.grochowski@stxnext.pl
  • piotr.bajsarowicz@stxnext.pl

Comments