summaryrefslogtreecommitdiff
path: root/scripts/build_web_resources.py
blob: 604d9d9b198b322b4620c203f2524b310b94b9f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
#!/usr/bin/python3
# coding=utf-8

"""This tool processes all problem files and database data, and outputs JSON
files to describe problems, to be used as static web resources.
Before running the script define the following environment variables, if defaults are not okay:
CODEQ_WEB_OUTPUT - directory where to write the output, defaults to /var/www/html/data
CODEQ_PROBLEMS - directory where you have codeq-problems checked out, defaults to /var/local/codeq-problems
CODEQ_DB_HOST - hostname or IP of the database server, defaults to localhost
CODEQ_DB_PORT - TCP port number of the database server, defaults to 5432
CODEQ_DB_DATABASE - name of the database, defaults to codeq
CODEQ_DB_USER - database username, defaults to codeq
CODEQ_DB_PASS - database password, defaults to c0d3q
"""

import os
import traceback
import sys
import json

# insert the parent directory, so the problem scripts find their modules
sys.path.insert(0, os.sep.join(os.path.dirname(__file__).split(os.sep)[:-1]))

import server.problems
import db

problems_path = os.environ.get('CODEQ_PROBLEMS') or '/var/local/codeq-problems'  # where to find problems, the same as server.problems._path_prefix
output_path = os.environ.get('CODEQ_WEB_OUTPUT') or '/var/www/html/data'  # the base directory where to create subdirectories and output the files for web

translations = {'sl', 'en'}  # translations to seek (sl.py, en.py, ...)

# default values (properties) for various types of items, also the list of properties to copy from modules
language_props = {  # for translation files inside the language subdirectory
    'name': 'Name not set',
    'description': 'Description not set',
    'hint': {}
}
language_common_props = {  # for common.py inside the language subdirectory
    'hint_type': {}  # type definitions of common hints
}
group_props = {  # for translation files inside the problem group subdirectory
    'name': 'Name not set',
    'description': 'Description not set'
}
group_common_props = {  # for common.py inside the problem group subdirectory
    'number': 1  # display index of the problem group
}
problem_props = {  # for translation files inside the problem subdirectory
    'name': 'Name not set',
    'slug': 'Slug not set',
    'description': 'Description not set',
    'plan': [],
    'hint': {}
}
problem_common_props = {  # for common.py inside the problem subdirectory
    'number': 1,  # display index of problems inside their groups
    'visible': True,  # whether the problem is enabled (disabled problems are excluded from the application)
    'hint_type': {}  # type definitions of problem hints
}

languages = {}  # programming languages, info from database
groups = {}  # problem groups, info from database

if not os.path.exists(output_path):
    os.mkdir(output_path)

conn = db.get_connection()
cur = conn.cursor()

def load_translation_data(package, defaults):
    result = {}
    path = os.sep.join(package.split('.'))
    for lang in translations:
        mod_path = os.path.join(problems_path, path, lang + '.py')
        if os.path.exists(mod_path) and os.path.isfile(mod_path):
            mod = None
            try:
                mod = server.problems.load_module(package + '.' + lang)
            except:
                traceback.print_exc()
            lang_data = {}
            result[lang] = lang_data
            if mod is None:
                print('Could not load module {}'.format(package + '.' + lang))
                for prop, default in defaults.items():
                    lang_data[prop] = default
            else:
                for prop, default in defaults.items():
                    lang_data[prop] = getattr(mod, prop, default)
    return result

def load_common_data(package, defaults):
    result = {}
    path = os.sep.join(package.split('.'))
    mod_path = os.path.join(problems_path, path, 'common.py')
    if os.path.exists(mod_path) and os.path.isfile(mod_path):
        mod = None
        try:
            mod = server.problems.load_module(package + '.common')
        except:
            traceback.print_exc()
        if mod is None:
            print('Could not load module {}'.format(package + '.common'))
            for prop, default in defaults.items():
                result[prop] = default
        else:
            for prop, default in defaults.items():
                result[prop] = getattr(mod, prop, default)
    else:
        print('Module {} does not exist'.format(package + '.common'))
        for prop, default in defaults.items():
            result[prop] = default
    return result

def process_hint_type(hint_type):
    result = {}
    for identifier, hint in hint_type.items():
        result[identifier] = hint.hint_type
    return result

cur.execute('select id, name, identifier from language')
row = cur.fetchone()
while row:
    languages[row[0]] = {'id': row[0], 'name': row[1], 'identifier': row[2]}
    row = cur.fetchone()

cur.execute('select id, name, identifier from problem_group')
row = cur.fetchone()
while row:
    groups[row[0]] = {'id': row[0], 'name': row[1], 'identifier': row[2]}
    row = cur.fetchone()

cur.execute('select id, language_id, problem_group_id, identifier from problem where is_visible = true order by language_id, problem_group_id')
previous_language_id = None
previous_group_id = None
lang_output_path = None
lang_index = None
row = cur.fetchone()

def dump_language_defs():
    if lang_index:
        # sort groups and problems
        groups = lang_index['groups']
        for group in groups:
            group['problems'].sort(key=lambda p: p.get('number', 0))
        groups.sort(key=lambda p: p.get('number', 0))
        # write out the JSON file
        with open(os.path.join(lang_output_path, 'language.json'), 'w') as f:
            json.dump(lang_index, f, indent=2)

while row:
    # process language data, it all goes into the language directory language.json
    try:
        language_id = row[1]
        if previous_language_id != language_id:
            language = languages[language_id]
            lang_identifier = language['identifier']
            language_path = os.path.join(problems_path, lang_identifier)
            if not (os.path.exists(language_path) and os.path.isdir(language_path)):
                print('ERROR: the directory for language {0} does not exist: {1}'.format(lang_identifier, language_path))
                continue
            dump_language_defs()  # dump the previous language index
            lang_output_path = os.path.join(output_path, lang_identifier)
            if not os.path.exists(lang_output_path):
                os.mkdir(lang_output_path)
            previous_language_id = language_id
            problem_groups_list = []
            lang_index = load_common_data(lang_identifier, language_common_props)
            lang_index['hint_type'] = process_hint_type(lang_index.get('hint_type', {}))  # process type definitions for common hints
            lang_index['id'] = language_id
            lang_index['identifier'] = lang_identifier
            lang_index['groups'] = problem_groups_list
            lang_index['translations'] = load_translation_data(lang_identifier, language_props)
            previous_group_id = None

        # process problem group data, it all goes into the language directory language.json
        group_id = row[2]
        if previous_group_id != group_id:
            group = groups[group_id]
            group_identifier = group['identifier']
            group_path = os.path.join(language_path, 'problems', group_identifier)
            if not (os.path.exists(group_path) and os.path.isdir(group_path)):
                print('ERROR: the directory for group {0}/{1} does not exist: {2}'.format(lang_identifier, group_identifier, group_path))
                continue
            group_output_path = os.path.join(lang_output_path, group_identifier)
            if not os.path.exists(group_output_path):
                os.mkdir(group_output_path)
            group_package = lang_identifier + '.problems.' + group_identifier
            previous_group_id = group_id
            problems_list = []
            group_data = load_common_data(group_package, group_common_props)
            group_data['id'] = group_id
            group_data['identifier'] = group_identifier
            group_data['problems'] = problems_list
            group_data['translations'] = load_translation_data(group_package, group_props)
            problem_groups_list.append(group_data)

        # process problem data, from common.py goes into the language directory language.json, others go into problem subdirectory's problem.json
        problem_id = row[0]
        problem_identifier = row[3]
        problem_path = os.path.join(group_path, problem_identifier)
        if not (os.path.exists(problem_path) and os.path.isdir(problem_path)):
            print('ERROR: the directory for problem {0}/{1}/{2} does not exist: {3}'.format(lang_identifier, group_identifier, problem_identifier, group_path))
            continue
        problem_package = group_package + '.' + problem_identifier
        # load common data, for the language directory
        common_data = load_common_data(problem_package, problem_common_props)
        if not common_data['visible']:
            continue  # problem is not visible, do not generate anything
        del common_data['visible']  # we don't need this field in the GUI
        hint_type = process_hint_type(common_data['hint_type'])  # save for later, to be used in problem_data below
        del common_data['hint_type']  # we don't need this field in the language index
        common_data['id'] = problem_id
        common_data['identifier'] = problem_identifier
        problems_list.append(common_data)
        # load translations, and copy only problem names to the common data
        problem_translations = load_translation_data(problem_package, problem_props)
        name_translations = {}
        for key, value in problem_translations.items():
            name_translations[key] = {'name': value.get('name')}
        common_data['translations'] = name_translations
        # dump translations, for the problem subdirectory's problem.json
        problem_data = {'id': problem_id, 'identifier': problem_identifier, 'translations': problem_translations, 'hint_type': hint_type}
        problem_output_path = os.path.join(group_output_path, problem_identifier)
        if not os.path.exists(problem_output_path):
            os.mkdir(problem_output_path)
        with open(os.path.join(problem_output_path, 'problem.json'), 'w') as f:
            json.dump(problem_data, f, indent=2)
    finally:
        row = cur.fetchone()

dump_language_defs()  # dump the last language index