5.10.2010

хроники django или homebudget part 2

Итак, как обстоят дела: после первого рывка я скурпулезено заполнял базу через существующие странички. Иногда когда скрипели зубы - добавлял функциональность. А так могу сказать - сейчас меня более-менее устраивает, все работает как и должно, есть куда развивать систему, следующие шаги ясны. Собственно, вот продолжение хроник, информация восстановлена по change-ам. Все измения фискируются в source-control'е в качестве которого я успешно использую bazaar.

  1. Добавляем просмотр отдельного дня. То есть это выборка из базы по конкретному дню, ссылки на редактирование отдельных записей (покупок), собственно ценники, и сумма за день. Как - добавляем новый метод в контроллер, добавляем отдельную вьюху для html-я, добавляем правило для урлов. all right, let's get started
  2. Чтобы было понятно - начинаем с правила для урлов. хотим чтобы запрос для странички дня выглядел типа blah-blah-blah/view/day/2010/04/30 ну типа никаких идиотских GET параметров. В urls.py добавляем следующее правило:
    (r'^purchases/view/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', 'day_view')
    
  3. Новый метод контроллера (в файле purchases/views.py), логика такая - добавленное в предыдущем пункте правило добавит необходимые параметры день/месяц/год, мы из них делаем нормальный объект datetime (если не получается - типа 68 января - то 404), потом забираем из базы все записи Purchase с этой датой, берем сумму по ценам и выводим все во вьюху.
    def day_view(request, year, month, day):
        try:
            dt = datetime.date(year=int(year), month=int(month), day=int(day))
        except:
            raise Http404
        plist = Purchase.objects.filter(purchase_date = dt)
        price_sum = Decimal('0.0')
        for it in plist:
            price_sum += it.price_total
    
        return render_to_response('purchases/day_view.html', {
            'date': dt,
            'list': plist,
            'price_sum': price_sum,
        })
    
  4. Создаем новый темплейт вьюхи templates/purchases/day_view.html с примерно следующим содержимым. Логика такая - пробегаем по списку значений как на главной странице, выводим ссылки на изменение и тд, выводим отдельно заранее посчитанный тотал.
    <p>Date: {{date}} </p>
    {% if list %}
        <ul>
            {% for p in list %}
                <li>
                    {{p.name}} 
                    <b>{{p.price_total}}</b> 
                    <a href='/purchases/edit/{{p.pk}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    <p>
        Total: <b>{{price_sum}}</b>
    </p>
    
    <a href='/purchases/add/'>Add</a>
    
  5. Проверяем на работоспособность. Должно работать :)
  6. Теперь делаем тоже самое но для месяца. Тупо по аналогии.
  7. Добавляем аналогично просмотру дня правило для просмотра месяца (доступ по ссылке типа blah-blah-blah/view/day/2010/04/):
    (r'^purchases/view/(?P<year>\d{4})/(?P<month>\d{1,2})/$', 'month_view')
    
  8. В методе контроллера имеем сразу же несколько проблем. Первая - нужно сделать запрос на совпадение и года, и месяца одновременно (в случае с просмотром дня - все было просто - тупо совпадение даты). Лечим следующим методом:
    from django.db.models import Q
    plist = Purchase.objects.filter(Q(purchase_date__year=dt.year) & Q(purchase_date__month=dt.month))
    
  9. Вторая проблема - нет в стандартной библиотеке пайтона функций типа добавить месяц. Можно есть - добавления отрезка времени, но так как в месяцах непостоянное кол-во дней такой способ выглядит очень корявым. Какой по-проще способ нашел - по дате составить новую с месяцем+1.
    # dt - дата
    y, m, d = dt.timetuple()[:3]
    dt_next = datetime.date(year=y+((m+1)/12), month=(m+1)%12, day=d)
    
  10. Третья проблема - как передать данные во вьюху. Я выбрал способ через словарь (dictionary) - в котором date => purchase_item.
  11. Вот получившийся код. Логика такая - получаем год/месяц - строим дату как первое число месяца. Выбираем из базы все записи purchase за этот месяц. Потом прибавляя по дню пока не прийдем к первому числу следующего месяца, попутно собирая суммы за каждый день и сумму за весь месяц.
    def month_view(request, year, month):
        try:
            dt = datetime.date(year=int(year), month=int(month), day=1)
        except:
            raise Http404
        from django.db.models import Q
        plist = Purchase.objects.filter(Q(purchase_date__year=dt.year) & Q(purchase_date__month=dt.month)).order_by('purchase_date')
    
        y, m, d = dt.timetuple()[:3]
        dt_next = datetime.date(year=y+((m+1)/12), month=(m+1)%12, day=d)
        day_price = {}
        month_total = Decimal('0.0')
        while (dt < dt_next):
            purchases_day = plist.filter(purchase_date__day=dt.day)
            day_total = Decimal('0.0')
            for it in purchases_day:
                day_total += it.price_total
            month_total += day_total
            day_price[dt] = day_total
            dt += datetime.timedelta(days=1)
    
        return render_to_response('purchases/month_view.html', {
                'date': datetime.date(year=int(year),month=int(month),day=1),
                'dict': day_price,
                'total': month_total,
            })
    
  12. Создаем новый темплейт templates/purchases/month_view.html
    <p>Date: {{date}} </p>
    {% if dict %}
        <ul>
            {% for key,value in dict.items %}
                <li>
                    {{key}} 
                    <b>{{value}}</b> 
                    <a href='/purchases/view/{{key.year}}/{{key.month}}/{{key.day}}/'>Edit</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>No purchases yet</p>
    {% endif %}
    <p>
        Total: <b>{{total}}</b>
    </p>
    
    <a href='/purchases/add/'>Add</a>
    
  13. Проверяем. Опа, не работает - все дни вперемешку, хотя и выставлено для запроса order_by. А тут начинаются темные, не хорошо документированные особенности джанги по сортировке словаря (я об этот момент чуть голову не сломал). Есть миллион решений - типа сделать буферный список из отсортированных ключей и его передавать с моделью, и тп. Но вот чтобы применить к for-у для словаря сортировку прямо в темплейте - нашел только один:
    for key,value in dict.items|dictsort:"0"
    
  14. Сделаем так чтобы после Edit-а или Add-а происходил редирект на просмотр использованного дня.
  15. Метод Add-а:
    def purchase_add(request):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                form.save()
                purchase_date = form.cleaned_data["purchase_date"]
                return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3]) 
        else:
            form = PurchaseItemForm()
    
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  16. Метод edit-а:
    def purchase_edit(request, purchase_id):
        try:
            item = Purchase.objects.get(pk=purchase_id)
        except Purchase.DoesNotExist:
            raise Http404
    
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST, instance=item)
            form.save()
            purchase_date = form.cleaned_data["purchase_date"]
            return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3])
        
        form = PurchaseItemForm(instance=item)
        return render_to_response('purchases/purchase_edit.html', {
            'form': form,
            'id' : purchase_id
        })
    
  17. Делаем добавление покупки на конкретный день (а не только на сегодня) - те если хотим добавить покупку за вчера со страницы вчерашнего дня - то поле дата должно быть корректно подставлено на вчера. Другими словами blah-blah-blah/purchases/add/ - должно добавлять покупку на сегодня, а blah-blah-blah/purchases/add/2010/04/30 - должно подставлять 2010.04.30
  18. В urls.py добавляем следующий словарь для определения сегодня:
    import datetime
    year, month, day = datetime.datetime.today().timetuple()[:3]
    today_dict = {'year':year, 'month':month, 'day':day}
    
    И следующее правила взамен старого add-а:
        (r'^purchases/add/$', 'purchase_add', today_dict),
        (r'^purchases/add/(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$', 'purchase_add'),
    
  19. Метод add должен теперь выглядеть так:
    def purchase_add(request, year, month, day):
        if request.method == 'POST':
            form = PurchaseItemForm(request.POST)
            if form.is_valid():
                form.save()
                purchase_date = form.cleaned_data["purchase_date"]
                return HttpResponseRedirect('/purchases/view/%s/%s/%s/' % purchase_date.timetuple()[:3]) 
        else:
            try:
                dt = datetime.date(year=int(year), month=int(month), day=int(day))
            except:
                dt = datetime.today() 
            purchaseitem = Purchase()
            purchaseitem.purchase_date = dt
            form = PurchaseItemForm(instance=purchaseitem)
    
        return render_to_response('purchases/purchase_add.html', {
            'form': form
        })
    
  20. Ну и собственно линка внутри templates/purchases/day_view.html
    <a href='/purchases/add/{{date.year}}/{{date.month}}/{{date.day}}/'>Add</a>
    

Посты по теме:

Комментариев нет: