Projet

Général

Profil

0002-move-convert_to_sql-to-management-command-20410.patch

Christophe Siraut, 21 juin 2018 20:24

Télécharger (16,2 ko)

Voir les différences:

Subject: [PATCH 2/2] move convert_to_sql to management command (#20410)

 tests/test_convert_to_sql.py                  | 145 +++++++++++++++++++++++++
 wcs/ctl/convertsql.py                         | 118 ++------------------
 wcs/ctl/management/commands/convert_to_sql.py | 151 ++++++++++++++++++++++++++
 3 files changed, 305 insertions(+), 109 deletions(-)
 create mode 100644 tests/test_convert_to_sql.py
 create mode 100644 wcs/ctl/management/commands/convert_to_sql.py
tests/test_convert_to_sql.py
1
import collections
2
import os
3
import random
4
import psycopg2
5
import pytest
6

  
7
from django.core.management import get_commands
8
from django.core.management import call_command
9
from django.core.management.base import CommandError
10

  
11
from wcs.sql import cleanup_connection
12
from wcs.formdef import FormDef
13
from wcs.fields import BoolField
14
from wcs.ctl.convertsql import CmdConvertToSql
15

  
16
from utilities import clean_temporary_pub
17
from utilities import create_temporary_pub
18
from utilities import force_connections_close
19
from test_api import local_user
20

  
21

  
22
class Struct:
23
    def __init__(self, **entries):
24
        self.__dict__.update(entries)
25

  
26

  
27
@pytest.fixture
28
def formdeffix():
29
    FormDef.wipe()
30
    formdef = FormDef()
31
    formdef.name = 'testform'
32
    formdef.description = 'plop'
33
    formdef.fields = [BoolField(id='1')]
34
    formdef.store()
35

  
36
    data_class = formdef.data_class()
37
    for value in (True, True, True, False):
38
        formdata = data_class()
39
        formdata.data = {'1': value}
40
        formdata.store()
41

  
42
    return formdef
43

  
44

  
45
@pytest.fixture(scope='module')
46
def cursor():
47
    conn = psycopg2.connect(user=os.environ.get('USER'))
48
    conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
49
    cur = conn.cursor()
50
    yield cur
51
    cur.close()
52

  
53

  
54
@pytest.fixture
55
def database(cursor):
56
    dbname = 'wcstests%d' % random.randint(0, 100000)
57
    cursor.execute('CREATE DATABASE %s' % dbname)
58
    yield dbname
59
    cleanup_connection()
60
    cursor.execute('DROP DATABASE %s' % dbname)
61

  
62

  
63
@pytest.fixture()
64
def pub(request):
65
    pub = create_temporary_pub(sql_mode=False)
66
    yield pub
67
    clean_temporary_pub()
68

  
69

  
70
def test_command_exists():
71
    assert 'convert_to_sql' in get_commands()
72

  
73

  
74
def test_unknown_publisher_fails():
75
    with pytest.raises(CommandError) as excinfo:
76
        call_command('convert_to_sql', '-d', 'unknown.net', '--database', 'foobar')
77
    assert excinfo.value.message == 'unknown tenant'
78

  
79

  
80
def test_failing_connection(pub):
81
    with pytest.raises(psycopg2.OperationalError) as excinfo:
82
        call_command('convert_to_sql', '-d', 'example.net', '--database', 'foobar', '--port', '666')
83
    assert 'could not connect' in excinfo.value.message
84

  
85

  
86
def test_database_does_not_exist(pub):
87
    new_database = 'test_{}'.format(random.randint(1000, 9999))
88
    with pytest.raises(psycopg2.OperationalError) as excinfo:
89
        call_command('convert_to_sql', '-d', 'example.net', '--database', new_database)
90
    assert 'does not exist' in excinfo.value.message
91

  
92

  
93
def test_already_migrated_fails():
94
    pub = create_temporary_pub(sql_mode=True)
95
    with pytest.raises(CommandError) as excinfo:
96
        call_command('convert_to_sql', '-d', 'example.net', '--database', 'foobar')
97
    assert excinfo.value.message == 'tenant already using postgresql'
98
    cleanup_connection()
99
    force_connections_close()
100
    clean_temporary_pub()
101

  
102

  
103
def test_setup_database(pub, database):
104
    call_command('convert_to_sql', '-d', 'example.net', '--database', database)
105
    pub.set_config()
106
    assert pub.cfg['postgresql'].get('database') == database
107

  
108

  
109
def test_migration(pub, database):
110
    assert 'postgresql' not in pub.cfg
111
    call_command('convert_to_sql', '-d', 'example.net',
112
                    '--database', database)
113
    pub.set_config()
114
    assert 'postgresql' in pub.cfg
115

  
116

  
117
def test_data_is_migrated(pub, database, local_user, formdeffix):
118
    pub.load_site_options()
119
    assert not pub.site_options.has_option('options', 'postgresql')
120
    call_command('convert_to_sql', '-d', 'example.net', '--database', database)
121
    pub.load_site_options()
122
    assert pub.site_options.has_option('options', 'postgresql')
123
    assert len(pub.user_class.get_users_with_name_identifier('0123456789')) == 1
124
    formdefs = FormDef.select()
125
    assert len(formdefs) == 1
126
    data_class = formdefs[0].data_class(mode='sql')
127
    assert len(data_class.keys()) == 4
128

  
129

  
130
def test_compat_previous_ctl_command(pub, database, local_user, formdeffix):
131
    pub.load_site_options()
132
    assert not pub.site_options.has_option('options', 'postgresql')
133
    cmd = CmdConvertToSql()
134
    cmd.config.add_section('main')
135
    cmd.config.set('main', 'error_log', '')
136
    cmd.config.set('main', 'app_dir', os.path.dirname(pub.app_dir))
137
    suboptions = Struct(**{'data_dir': pub.app_dir, 'dbname': database, 'user': os.environ.get('USER')})
138
    cmd.execute(object, suboptions, ('example.net',))
139
    pub.load_site_options()
140
    assert pub.site_options.has_option('options', 'postgresql')
141
    assert len(pub.user_class.get_users_with_name_identifier('0123456789')) == 1
142
    formdefs = FormDef.select()
143
    assert len(formdefs) == 1
144
    data_class = formdefs[0].data_class(mode='sql')
145
    assert len(data_class.keys()) == 4
wcs/ctl/convertsql.py
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import os
18
import sys
19
import traceback
20

  
21

  
22
num_columns = 0
23
try:
24
    import curses
25
except ImportError:
26
    curses = None
27
else:
28
    try:
29
        curses.setupterm()
30
        num_columns = curses.tigetnum('cols')
31
    except:
32
        pass
33

  
34

  
35 18
from qommon.ctl import Command, make_option
19
from django.core.management import call_command
36 20

  
37
def update_progress(progress):
38
    if not num_columns:
39
        return
40
    sys.stdout.write('[%s] %s%%\r' % (
41
        ('#'*((num_columns-10)*progress/100)).ljust(num_columns-15), progress))
42 21

  
43 22
class CmdConvertToSql(Command):
44 23
    name = 'convert-to-sql'
......
69 48
        pub.app_dir = os.path.join(pub.app_dir, hostname)
70 49
        pub.set_config()
71 50

  
72
        from wcs.formdef import FormDef
73
        from wcs import sql
74

  
75
        if sub_options.port:
76
            sub_options.port = int(sub_options.port)
77

  
78
        pub.cfg['postgresql'] = {
79
                'database': sub_options.dbname,
80
                'user': sub_options.user,
81
                'password': sub_options.password,
82
                'host': sub_options.host,
83
                'port': sub_options.port,
84
        }
85

  
86
        sql.get_connection_and_cursor(new=True)
87

  
88
        errors = []
89

  
90
        print 'converting users'
91
        from users import User
92
        sql.do_user_table()
93
        count = User.count()
94
        for i, user_id in enumerate(User.keys()):
95
            user = User.get(user_id)
96
            user.__class__ = sql.SqlUser
97
            try:
98
                user.store()
99
            except AssertionError:
100
                errors.append((user, traceback.format_exc()))
101
            update_progress(100*i/count)
102
        sql.SqlUser.fix_sequences()
103

  
104
        if errors:
105
            error_log = file('error_user.log', 'w')
106
            for user, trace in errors:
107
                print >> error_log, 'user_id', user.id
108
                print >> error_log, trace
109
                print >> error_log, '-'*80
110
            error_log.close()
111
            print 'There were some errors, see error_user.log for details.'
112

  
113
        errors = []
114
        for formdef in FormDef.select():
115
            print ('converting %s' % formdef.name).ljust(num_columns-1)
116
            sql.do_formdef_tables(formdef, rebuild_views=True,
117
                    rebuild_global_views=True)
118
            data_class = formdef.data_class(mode='files')
119
            count = data_class.count()
120

  
121
            # load all objects a first time, to allow the migrate() code to be
122
            # run and the eventual changes properly saved.
123
            for id in data_class.keys():
124
                formdata = data_class.get(id)
125
            delattr(sys.modules['formdef'], formdef.url_name.title())
126

  
127
            # once this is done, reload and store everything in postgresql
128
            sql_data_class = formdef.data_class(mode='sql')
129
            for i, id in enumerate(data_class.keys()):
130
                formdata = data_class.get(id)
131
                formdata._formdef = formdef
132
                formdata._evolution = formdata.evolution
133
                formdata.__class__ = sql_data_class
134
                try:
135
                    formdata.store()
136
                except AssertionError:
137
                    errors.append((formdata, traceback.format_exc()))
138
                update_progress(100*i/count)
139
            sql_data_class.fix_sequences()
140

  
141
        print 'done'.ljust(num_columns-1)
142

  
143
        sql.do_tracking_code_table()
144
        sql.do_session_table()
145
        sql.do_meta_table()
146

  
147
        if errors:
148
            error_log = file('error_formdata.log', 'w')
149
            for formdata, trace in errors:
150
                print >> error_log, formdata.formdef, formdata.id
151
                print >> error_log, trace
152
                print >> error_log, '-'*80
153
            error_log.close()
154
            print 'There were some errors, see error_formdata.log for details.'
155

  
156
        if not pub.has_site_option('postgresql'):
157
            print 'You still have to edit your site-options.cfg'
51
        cmd = ['convert_to_sql',
52
               '-d', hostname,
53
               '--database', sub_options.dbname]
158 54

  
159
        pub.write_cfg()
55
        for k in ['user', 'password', 'host', 'port']:
56
            value = getattr(sub_options, k, False)
57
            if value:
58
                cmd.extend(['--%s' % k, value])
160 59

  
60
        call_command(*cmd)
161 61

  
162 62
CmdConvertToSql.register()
wcs/ctl/management/commands/convert_to_sql.py
1
# w.c.s. - web application for online forms
2
# Copyright (C) 2005-2018  Entr'ouvert
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16

  
17
import os
18
import sys
19
import StringIO
20
import traceback
21

  
22
from django.core.management.base import BaseCommand
23
from django.core.management.base import CommandError
24

  
25
from qommon.publisher import get_publisher_class
26

  
27
from wcs import sql
28
from wcs.formdef import FormDef
29
from wcs.qommon.storage import atomic_write
30
from wcs.users import User
31

  
32

  
33
class Command(BaseCommand):
34

  
35
    help = 'Setup postgresql connection parameters and migrate existing objects.'
36

  
37
    def add_arguments(self, parser):
38
        parser.add_argument('-d', '--domain', required=True)
39
        parser.add_argument('--database', required=True)
40
        parser.add_argument('--host')
41
        parser.add_argument('--port', type=int)
42
        parser.add_argument('--user')
43
        parser.add_argument('--password')
44

  
45
    def handle(self, **options):
46
        self.publisher = self.get_publisher(options['domain'])
47
        if self.publisher.is_using_postgresql():
48
            raise CommandError('tenant already using postgresql')
49

  
50
        self.setup_connection(**options)
51
        sql.get_connection(new=True)
52
        self.store_users()
53
        self.store_forms()
54
        self.publisher.write_cfg()
55
        self.enable_connection()
56
        self.publisher.cleanup()
57

  
58
    def get_publisher(self, domain):
59
        publisher_class = get_publisher_class()
60
        publisher = publisher_class.create_publisher()
61
        if domain not in publisher.get_tenants():
62
            raise CommandError('unknown tenant')
63

  
64
        publisher.app_dir = os.path.join(publisher.app_dir, domain)
65
        publisher.set_config()
66
        return publisher
67

  
68
    def setup_connection(self, **kwargs):
69
        options = {}
70
        for k in ['host', 'port', 'database', 'user', 'password']:
71
            if k in kwargs:
72
                options[k] = kwargs.get(k)
73
        self.publisher.cfg['postgresql'] = options
74

  
75
    def enable_connection(self):
76
        if not self.publisher.site_options.has_option('options', 'postgresql'):
77
            self.publisher.site_options.set('options', 'postgresql', 'true')
78
            options_file = os.path.join(self.publisher.app_dir, 'site-options.cfg')
79
            stringio = StringIO.StringIO()
80
            self.publisher.site_options.write(stringio)
81
            atomic_write(options_file, stringio.getvalue())
82

  
83
    def store_users(self):
84
        errors = []
85
        print('converting users')
86
        sql.do_user_table()
87
        count = User.count()
88
        for i, user_id in enumerate(User.keys()):
89
            user = User.get(user_id)
90
            user.__class__ = sql.SqlUser
91
            try:
92
                user.store()
93
            except AssertionError:
94
                errors.append((user, traceback.format_exc()))
95
            self.update_progress(100*i/count)
96
        sql.SqlUser.fix_sequences()
97

  
98
        if errors:
99
            error_log = open('error_user.log', 'w')
100
            for user, trace in errors:
101
                error_log.write('user_id {}'.format(user.id))
102
                error_log.write(trace)
103
                error_log.write('-'*80)
104
            error_log.close()
105
            print('There were some errors, see error_user.log for details.')
106

  
107
    def store_forms(self):
108
        errors = []
109
        for formdef in FormDef.select():
110
            print('converting %s' % formdef.name)
111
            sql.do_formdef_tables(formdef, rebuild_views=True,
112
                                  rebuild_global_views=True)
113
            data_class = formdef.data_class(mode='files')
114
            count = data_class.count()
115

  
116
            # load all objects a first time, to allow the migrate() code to be
117
            # run and the eventual changes properly saved.
118
            for id in data_class.keys():
119
                formdata = data_class.get(id)
120
            delattr(sys.modules['formdef'], formdef.url_name.title())
121

  
122
            # once this is done, reload and store everything in postgresql
123
            sql_data_class = formdef.data_class(mode='sql')
124
            for i, id in enumerate(data_class.keys()):
125
                formdata = data_class.get(id)
126
                formdata._formdef = formdef
127
                formdata._evolution = formdata.evolution
128
                formdata.__class__ = sql_data_class
129
                try:
130
                    formdata.store()
131
                except AssertionError:
132
                    errors.append((formdata, traceback.format_exc()))
133
                self.update_progress(100*i/count)
134
            sql_data_class.fix_sequences()
135

  
136
        sql.do_tracking_code_table()
137
        sql.do_session_table()
138
        sql.do_meta_table()
139

  
140
        if errors:
141
            error_log = open('error_formdata.log', 'w')
142
            for formdata, trace in errors:
143
                error_log.write('{} {}'.format(formdata.fromdef, formdata.id))
144
                error_log.write(trace)
145
                error_log.write('-'*80)
146
            error_log.close()
147
            print('There were some errors, see error_formdata.log.')
148

  
149
    def update_progress(self, progress, num_columns=120):
150
        sys.stdout.write('[%s] %s%%\r' % (
151
            ('#'*((num_columns-10)*progress/100)).ljust(num_columns-15), progress))
0
-