Система для работы с законодательной базой.

Постановка задачи.

Даны две таблицы с данными.

Список законопроектов с номером датой и кратким описанием.

Список документов в формате txt.

Необходимо внести документы в отдельную таблицу и связать с законопроектами по номеру договора.

Сделать новую таблицу с персонами (ФИО, должность, дата рождения) и предоставить интерфейс для ее заполнения.

Сделать форму поиска документов по регионам, ФИО, судам.

Сделать возможность создания связи персон по родству.

Технологии.

  • PostgreSQL

  • Django

  • Angular 7

Cсылка на репозиторий GIT

Старт проекта.

mkdir dj-loyer
cd dj-loyer

GIT

git init
nano .gitignore

Вписываем в игнор виртуальное окружение питона и файлы *.pyc

venv
*.pyc

записываем в файл ctrl+o выходим из nano ctrl+x

Виртуальное окружение

установка в систему нужных команд (установщик python pip и virtualenv)

sudo apt-get install python3-pip virtualenv

установка виртуального окружения в проект

virtualenv -p python3 venv
  • создается папка venv где будут requirements-ы, которую обычно игнорят в git.

Активация виртуального окружения

. ./venv/bin/activate

появляется приставка (venv) в начале коммандной строки что значит что окружение активировано и мы можем устанавливать в него необходимы пакеты

Установка Django

echo "django" >> requirements.txt
pip install -r requirements.txt

Создание нового проекта

django-admin startproject dj_prj

Создание приложения

cd dj_prj
./manage.py startapp app

Прописываем название нового приложения в настройках djangoprj/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app'
]

Запуск сервера разработки.

./manage.py runserver 9898

Подключение к базе PostgreSQL

pip install psycopg2

Коннект в settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'dj-loyer',
        'USER': 'postgres',
        'PASSWORD': '***',
        'HOST': 'localhost',
    }
}

Создание таблиц.

./manage.py migrate

Создание суперпользователя для админки.

./manage.py createsuperuser

telegram bot page

Установка и обновление nodejs и npm.

npm install npm@latest -g
sudo npm cache clean -f
sudo npm install -g n
sudo n stable

Создание проекта.

sudo npm install -g @angular/cli
ng new ng-prj

Запуск сервера разработки

cd ng-prj
ng serve

http://localhost:

Если наблюдается ошибка Invalid Host header то запускать с параметром –disable-host-check

ng serve  --disable-host-check

Использование ng-bootstrap.

Ссылка да документацию

Пакет содержит ангуляровские директивы и исключает использование jquery.

npm install --save @ng-bootstrap/ng-bootstrap

Получили

+ @ng-bootstrap/ng-bootstrap@4.1.1
added 1 package from 1 contributor and audited 42613 packages in 13.343s
found 1 high severity vulnerability

Попробуем устрани уязвимость.

up to date in 9.437s
fixed 0 of 1 vulnerability in 42613 scanned packages
  1 vulnerability required manual review and could not be updated

Установим сам bootstrap.

npm install bootstrap@4.0.0-alpha.6 tether --save

Результат.

+ tether@1.4.6
+ bootstrap@4.0.0-alpha.6

Добавим файл css bootstrap в конфигурацию командного менеджера angular-cli.json

    "styles": [
      "src/styles.sass",
      "node_modules/bootstrap/dist/css/bootstrap.min.css"
    ],
    "scripts": [
      "node_modules/jquery/dist/jquery.min.js",
      "node_modules/tether/dist/js/tether.min.js",
      "node_modules/bootstrap/dist/js/bootstrap.min.js"

    ]

Было

bootstrap start

Стало

bootstrap start

В файле настроек главного модуля scr/app/app.module.ts добавим импорт библиотеки.

import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

....

  imports: [
    BrowserModule,
    NgbModule.forRoot()
],

Переделаем главный шаблон src/app/app.module.ts заменим все содержимое на.

<div class="navbar navbar-dark bg-dark box-shadow">
    <div class="container d-flex justify-content-between">
      <a href="#" class="navbar-brand d-flex align-items-center">
        <strong>Loyer service</strong>
      </a>

    </div>
</div>

Поменяем версию bootstrap

npm install bootstrap@4.0.0 tether --save

Установка jQuery (под bootstrap 4.0.0)

npm install jquery --save

Навигационное меню

Ссылка на документацию

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Navbar</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Dropdown
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
          <div class="dropdown-divider"></div>
          <a class="dropdown-item" href="#">Something else here</a>
        </div>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#">Disabled</a>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
</nav>

Постраничная навигация.

Определяем дополгительные переменные в компоненте.

export class DocumentComponent implements OnInit {

  items: any;
  count: number;
  next: string;
  prev: string;
  offset: number = 0;

Создадим функцию получения страницы.

  constructor(private http_service: DocumentService) { }

  getPage(offset: number) {
    this.http_service.getDocList(offset).subscribe((data: any) => {
      this.items = data['results'];
      this.count = data['count'];
      this.next = data['next'];
      this.prev = data['previous'];
      this.offset = offset;
    })
  }

Вызовем ее при инициализации компонента.

  ngOnInit() {

      this.getPage(this.offset);

  }

Доработаем сервис для использования новой переменной offset.

  getDocList(offset: number) {
    return this.http.get(API_URL+'api/documents?limit=10&offset='+offset);
  }

Вставляем кнопки в шаблон.

  <button *ngIf="prev" (click)="getPage(offset-10)"  >Previous</button>
  <button *ngIf="next" (click)="getPage(offset+10)"  >Next</button>

django rest framework angular pager

Изменим шаблон и выведем данные в таблицу.

  <div style="text-align: center">
    <button *ngIf="prev" (click)="getPage(offset-10)"  >Previous</button>
    <button *ngIf="next" (click)="getPage(offset+10)"  >Next</button>
  </div>


  <table class="table table-striped table-sm table-dark table-bordered  table-hover">
    <thead class="thead-dark">
      <tr>
        <th scope="col">#</th>
        <th scope="col">Date</th>
        <th scope="col">Title</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let item of items">
        <th>{{ item.id  }}</th>
        <td>{{ item.date  }}</td>
        <td>{{ item.title  }}</td>
      </tr>
    </tbody>
  </table>

  <div style="text-align: center">
    <button *ngIf="prev" (click)="getPage(offset-10)"  >Previous</button>
    <button *ngIf="next" (click)="getPage(offset+10)"  >Next</button>
  </div>

django rest framework angular pager

Создание компонента

ng g component login

Роутинг.

{ path: 'login', component: LoginComponent },

Ссылка

<a class="nav-link" routerLink="/login">Login</a>

Форма.

<div class="mx-auto login_form">
  <form method="post">
    <h2 class="text-center">Log in</h2>
    <div class="form-group">
        <input type="text" class="form-control" placeholder="Username" required="required">
    </div>
    <div class="form-group">
        <input type="password" class="form-control" placeholder="Password" required="required">
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary btn-block">Log in</button>
    </div>
    <div class="clearfix">
        <label class="pull-left checkbox-inline"><input type="checkbox"> Remember me</label>

    </div>
  </form>
</div>

Сервис

ng g service login/login

Подключение в главный модуль.

  providers: [ 
    DocumentService,
    LoginService  
  ],

Опишем сервис.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'
import { API_URL } from '../global';


@Injectable({
  providedIn: 'root'
})
export class LoginService {

  constructor(private http: HttpClient) { }

  login(login: string, password: string) {
    var data = {'login': login, 'password': password};
    return this.http.post(API_URL+'api/login', data);
  }

}

Изменим форму, добавив переменные модели.

    <input type="text" class="form-control" [(ngModel)]="username"  name="username" required="required">
    ...
    <input type="password" class="form-control" name="password" [(ngModel)]="password"  required="required">

Для работы [(ngModel)] включим FormsModule в секцию imports главного модуля.

  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    NgbModule.forRoot()
  ],

Доработаем компонент.

import { LoginService } from './login.service';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.sass']
})
export class LoginComponent implements OnInit {
  password: string;
  username: string;
  constructor(private http_service: LoginService) { }

  ngOnInit() {
  }

  login() {
      this.http_service.login(this.username, this.password).subscribe((data: any) => {
         console.log(data);
      })
  }

}

Вызовем метод при клике на кнопке.

<button type="submit" (click)="login()" class="btn btn-primary btn-block">Log in</button>

login angular form

Джанго вью для API запроса

Может быть оформлен как ViewSet или обычным View (APIView)

class UpdateTimeView(APIView):

    def get(self, request, format=None):
        return Response({})


class UpdateTimeViewSet(ViewSet):

    def list(self, request, format=None):
        return Response({})

При этом использование в роутинге будет отличаться.

router.register(r'documents', DocumentViewSet)

или

path('api/login', APIView.as_view()),

Сделаем через APIView.

from rest_framework.views import APIView
from rest_framework.response import Response

class LoginView(APIView):
    def post(self, request):
        return Response({"status": 0})

Роутинг.

path('api/login', LoginView.as_view()),

django api login

Опишем автоизацию.

from django.contrib.auth.models import User
...
class LoginView(APIView):
    def post(self, request):
        payload = json.loads(request.body.decode('utf-8'))
        try:
            user = User.objects.get(username=payload['login'])
            if not user.check_password(payload['password']):
                 return Response({"status": 1, "message": "Error, wrong password!"})
            else:
                return Response({"status": 0, "message": "ok"})
        except Exception as e:
            return Response({"status": 1, "message": "Error, wrong login!", "exeption": str(e)})

django api login

Создание нового компонента

ng g component nav

Перенесем шаблон из src/app/app.component.html в scr/app/nav/nav.component.html

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Loyer service</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Dropdown
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdown">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
          <div class="dropdown-divider"></div>
          <a class="dropdown-item" href="#">Something else here</a>
        </div>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#">Disabled</a>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
      <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
</nav>

Вмето него вставим компонент навигации в главный шаблон.

<app-nav></app-nav>

Сгенерируем новый компонент index для главной страницы.

ng g component index

Добавляем роутинг в app-routing.modules.ts

const routes: Routes = [
  { path: 'index', component: IndexComponent },
  { path: '',
  redirectTo: '/index',
  pathMatch: 'full'
}
];

Определим место вывода контента в главном шаблоне.

<router-outlet></router-outlet>

angular routing

Обрамим для отступов.

<app-nav></app-nav>
<div class="container">
  <router-outlet></router-outlet>
</div>

Сгенерируем новый компонент login для для формы ввода логина и пароля.

ng g component login

Установка пакета.

Официальная документация фреймворка REST

Добавляем пакеты в requirements.txt

django
psycopg2==2.7
djangorestframework
markdown
django-filter

Установка

pip install -r requirements.txt

Пропишем в settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'app',
    'rest_framework'
]

Установка пакета.

Официальная документация фреймворка REST

Для того чтобы подключить REST API к одной модели (таблице) необходимо произвести изменения в трех местах.

1. Сериализатор (app/serializer.py)

from rest_framework import serializers
from app.models import MainDocuments

class DocumentSerializer(serializers.ModelSerializer):
    class Meta:
        model = MainDocuments
        fields = ['title']

В сериализаторе мы определяем структуру выводимых данных json.

Привязываем модель и указываем список интересующих нас полей в модели.

Наша модель:

class MainDocuments(models.Model):
    uid = models.CharField(max_length=250)
    title = models.TextField(blank=True, null=True)
    href = models.CharField(max_length=250, blank=True, null=True)
    date = models.DateField(blank=True, null=True)
    is_error = models.BooleanField()
    is_file_downloaded = models.BooleanField()
    doc_html = models.TextField(blank=True, null=True)
    doc_txt = models.TextField(blank=True, null=True)
    is_document_downloaded = models.BooleanField()
    is_files_downloaded = models.BooleanField()

2. Вьюха (app/views.py)

from django.shortcuts import render
from rest_framework import viewsets
from app.models import MainDocuments
from app.serializer import DocumentSerializer

class DocumentViewSet(viewsets.ModelViewSet):
    queryset = MainDocuments.objects.all()
    serializer_class = DocumentSerializer

Определяем запрос к БД и привязываем класс серилизации.

Роутинг.

from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from app.views import DocumentViewSet

router = routers.DefaultRouter()
router.register(r'documents', DocumentViewSet)


urlpatterns = [
    path('api', include(router.urls)),
    path('admin/', admin.site.urls),
]

Пагинация (settings.py)

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 10
}

Точка входа http://localhost:8000/api

django rest framework
django rest framework

Создание компонента.

ng g component document

Роутинг (app-routing.modules.ts)

const routes: Routes = [
  { path: 'index', component: IndexComponent },
  { path: 'login', component: LoginComponent },
  { path: 'documents', component: DocumentComponent },
  { path: '',
  redirectTo: '/index',
  pathMatch: 'full'
}
];

Ссылка в шаблоне компонента навигации.

  <li class="nav-item active">
    <a class="nav-link" routerLink="/documents">Documents</a>
  </li>

Вывод тестовых данных (document/document.component.html).

    <li  *ngFor="let item of items" > {{ item.title  }}  </li>
    

Тестовые данные в компоненте.

import { Component, OnInit } from '@angular/core';


@Component({
  selector: 'app-document',
  templateUrl: './document.component.html',
  styleUrls: ['./document.component.sass']
})
export class DocumentComponent implements OnInit {

  items = [{"title": 'one'},{"title": "two"}]

  constructor() { }

  ngOnInit() {


  }

}

Результат.

django rest framework

Генерируем сервис.

ng g service document/document

Подключаем его вместе с HttpClientModule из коробки ангуляр в корневой модуль (app.module.ts).

import { DocumentService } from ‘./document/document.service’; import { HttpClientModule } from ‘@angular/common/http’;

@NgModule({
  declarations: [
    AppComponent,
    NavComponent,
    IndexComponent,
    LoginComponent,
    DocumentComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    NgbModule.forRoot()
  ],
  providers: [ DocumentService  ],
  bootstrap: [AppComponent]
})

Пишем сервис.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'

@Injectable({
  providedIn: 'root'
})
export class DocumentService {

  constructor(private http: HttpClient) { }

  getDocList() {
    return this.http.get('http://localhost:8989/api/documents');
  }

}

Используем сервис в компоненте (document/document.component.ts).

import { Component, OnInit } from '@angular/core';
import { DocumentService } from './document.service';

@Component({
  selector: 'app-document',
  templateUrl: './document.component.html',
  styleUrls: ['./document.component.sass']
})
export class DocumentComponent implements OnInit {

  constructor(private http_service: DocumentService) { }

  ngOnInit() {

      this.http_service.getDocList().subscribe((data: any) => {
        this.items = data['results'];
      })


  }

}

django rest framework

Создадим файл с глобальными настройками global.ts в папке app.

export const API_URL = ‘http://localhost:8989/’;

Использование внутри сервиса.

import { API_URL } from '../global';


@Injectable({
  providedIn: 'root'
})
export class DocumentService {

  constructor(private http: HttpClient) { }

  getDocList() {
    return this.http.get(API_URL+'api/documents');
  }

Анализ ответа авторизации.

login() { this.http_service.login(this.username, this.password).subscribe((data: any) => { console.log(data); if(data[‘status’] == 1) { alert(data[‘message’]); } else { localStorage.setItem(‘is_auth’, true); }

  })

}

Устанавливаем переменную is_auth в хранилище localStorage.

localstorage angular

Возвратим подпись пользователя от сервера при авторизации.

Для этого воспользуемся библиотекой rest_framework.authtoken из поставки Django REST framework.

Документация

class LoginView(APIView):
    def post(self, request):
        payload = json.loads(request.body.decode('utf-8'))
        try:
            user = User.objects.get(username=payload['login'])
            if not user.check_password(payload['password']):
                 return Response({"status": 1, "message": "Error, wrong password!"})
            else:
                try:
                    token = Token.objects.get(user=user)
                except:
                    token = Token.objects.create(user=user)
                return Response({
                                 "status": 0, 
                                 "message": "ok", 
                                 "token": token.key
                                 })
        except Exception as e:
            return Response({"status": 1, "message": "Error, wrong login!", "exeption": str(e)})

Сохраним токен в приложении ангуляр.

  login() {
      this.httpService.login(this.username, this.password).subscribe((data: any) => {
          console.log(data);
          if (data.status === 1) {
              alert(data.message);
          } else {
             localStorage.setItem('is_auth', 'true');
             localStorage.setItem('user_token', data.token);
          }

      });
  }

localstorage angular

Сделаем редирект на главную при успехе.

import {Router} from "@angular/router"
...
constructor(private http_service: LoginService, private router: Router) { }
...
  login() {
      this.http_service.login(this.username, this.password).subscribe((data: any) => {
         console.log(data);
         if(data['status'] == 1) {
          alert(data['message']);
        } else {
          localStorage.setItem('is_auth', true);
          localStorage.setItem('user_token', data.token);
          this.router.navigate(['/index'])
        }
      })
  }

Создание модели

class Person(models.Model):
    name_ru = models.CharField(max_length=250)
    name_kz = models.CharField(max_length=250)
    name_translit = models.CharField(max_length=250)
    birth = models.DateField()
    role = models.CharField(max_length=50)

Админ интерфейс.

from .models import MainDocuments, Person
...
class PersonAdmin(admin.ModelAdmin):
    list_display = ['name_ru', 'name_kz']
    search_fields = ['name_ru']


admin.site.register(Person, PersonAdmin)

chromedriver version

Создаем команду для парсинга документов для выбора ФИО (app/management/commands/parse_persons.py).

from django.core.management.base import BaseCommand, CommandError

class Command(BaseCommand):

    def handle(self, *args, **options):
        print('Parsing persons.')

Пробежим по таблице документов и выведем заголовки.

from django.core.management.base import BaseCommand, CommandError

from app.models import Person, MainDocuments

class Command(BaseCommand):

    def handle(self, *args, **options):
        print('Parsing persons.')   
        for md in MainDocuments.objects.all():
            print(md.title)

Попробуем вывести судей (строки от ‘Судья:’ до 3 пробела ) и сохранить их в таблице person, предварительно очистив.

from django.core.management.base import BaseCommand, CommandError

from app.models import Person, MainDocuments
import re

class Command(BaseCommand):

    def handle(self, *args, **options):
        print('Parsing persons.')   
        print("Deleting all persons")
        Person.objects.all().delete()
        for md in MainDocuments.objects.all():
            match = re.search('Судья:(.*)',md.title)
            try:
                rezult = match.group(1)    
                p = Person()
                p.name_ru = rezult
                p.role = 'judge'
                p.save()
                print("Saving ... %s" % rezult)
            except Exception as e:
                print(str(e))

Выбираем ответчиков по делу.

match = re.search('Ответчики по делу:(.*)Судья:',md.title)
try:
    rezult = match.group(1)   
    for nm in rezult.split(','):
        nm.replace('.','')
        if len(nm)>2:
            checking_words = nm.split(' ')
            if len(checking_words)<5:
                p = Person()
                p.name_ru = nm
                p.role = 'plantiff'
                p.save()
    print("Saving ... %s" % rezult)
except Exception as e:
    print(str(e))

Создаем ветку в гит и переключаемся на нее.

git branch model-document
git checkout model-document

Создаем модель из предоставленного дампа таблицы.

Разворачиваем дамп из файла

postgresql sql dump

postgresql sql dump

Делаем выгрузку классов таблиц из БД в файл.

./manage.py inspectdb >> source-model.py

При возникновении ошибки

The error was: sequence index must be integer, not 'slice'

Необходимо даунгрейдить psycopg2 до версии 2.7

# requirements.txt
django
psycopg2==2.7

Переставим командой

pip install -r ../requirements.txt

Заберем код модели и вставим в app/models.py нашего приложения.

class MainDocuments(models.Model):
    uid = models.CharField(max_length=250)
    title = models.TextField(blank=True, null=True)
    href = models.CharField(max_length=250, blank=True, null=True)
    date = models.DateField(blank=True, null=True)
    is_error = models.BooleanField()
    is_file_downloaded = models.BooleanField()
    doc_html = models.TextField(blank=True, null=True)
    doc_txt = models.TextField(blank=True, null=True)
    is_document_downloaded = models.BooleanField()
    is_files_downloaded = models.BooleanField()

    class Meta:
        managed = False
        db_table = 'main_documents'

Делаем миграцию.

./manage.py makemigration
./manage.py migrate

postgresql sql dump

Создаем админ интерфейс (файл app/admin.py)

from django.contrib import admin
from .models import MainDocuments
# Register your models here.

class MainDocumentsAdmin(admin.ModelAdmin):
    list_display = ['title', 'date']
    search_fields = ['title']
    list_filter = ['date']

admin.site.register(MainDocuments, MainDocumentsAdmin)

postgresql sql dump

Мержим ветку model-document с веткой master.

git checkout master
git merge model-document

postgresql sql dump

Задать вопрос, прокомментировать.