Passed
Push — main ( 411128...491da5 )
by torrua
01:45
created

loglan_db.model_html   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 381
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 199
dl 0
loc 381
ccs 137
cts 137
cp 1
rs 8.96
c 0
b 0
f 0
wmc 43

16 Methods

Rating   Name   Duplication   Size   Complexity  
A HTMLExportDefinition.highlight_key() 0 18 1
A HTMLExportDefinition.format_body() 0 18 1
A HTMLExportDefinition.export_for_loglan() 0 22 4
B HTMLExportDefinition.export_for_english() 0 48 6
A HTMLExportWord.html_origin() 0 20 4
A HTMLExportWord._get_stylized_words() 0 25 3
A HTMLExportWord.html_definitions() 0 8 1
A HTMLExportWord.__case_insensitive_words() 0 4 2
A HTMLExportWord.html_meaning() 0 23 4
A HTMLExportWord.get_styled_values() 0 30 2
A HTMLExportWord.__generate_origin() 0 18 3
A HTMLExportWord.__definitions_by_key() 0 10 2
B HTMLExportWord.html_all_by_name() 0 44 5
A HTMLExportWord.meaning() 0 18 1
A HTMLExportWord.translation_by_key() 0 26 2
A HTMLExportWord.__case_sensitive_words() 0 4 2

How to fix   Complexity   

Complexity

Complex classes like loglan_db.model_html often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2 1
"""
3
HTML Export extensions of LOD database models
4
"""
5 1
import os
6 1
from itertools import groupby
7 1
from typing import Optional, Union, List
8
9 1
from sqlalchemy import or_
10
11 1
from loglan_db.model_db.base_event import BaseEvent
12 1
from loglan_db.model_export import ExportWord, ExportDefinition
13
14 1
DEFAULT_HTML_STYLE = os.getenv("DEFAULT_HTML_STYLE", "ultra")
15
16
17 1
class HTMLExportDefinition(ExportDefinition):
18
    """
19
    HTMLExportDefinition Class
20
    """
21 1
    @staticmethod
22 1
    def format_body(body: str) -> str:
23
        """
24
        Substitutes tags in the definition's body
25
        Formats punctuation signs
26
        :param body:
27
        :return:
28
        """
29 1
        to_key = '<k>'  # key
30 1
        tc_key = '</k>'
31 1
        to_log = '<l>'  # log
32 1
        tc_log = '</l>'
33
34 1
        return body \
35
            .replace("<", "&lt;").replace(">", "&gt;") \
36
            .replace("«", to_key).replace("»", tc_key) \
37
            .replace("{", to_log).replace("}", tc_log) \
38
            .replace("...", "…").replace("--", "—")
39
40 1
    @staticmethod
41 1
    def highlight_key(def_body, word) -> str:
42
        """
43
        Highlights the current key from the list, deselecting the rest
44
        :param def_body:
45
        :param word:
46
        :return:
47
        """
48
49 1
        to_key = '<k>'  # key
50 1
        tc_key = '</k>'
51
52 1
        word_template_original = f'{to_key}{word}{tc_key}'
53 1
        word_template_temp = f'<do_not_delete>{word}</do_not_delete>'
54 1
        def_body = def_body.replace(word_template_original, word_template_temp)
55 1
        def_body = def_body.replace(to_key, "").replace(tc_key, "")
56 1
        def_body = def_body.replace(word_template_temp, word_template_original)
57 1
        return def_body
58
59 1
    def export_for_english(self, word: str, style: str = DEFAULT_HTML_STYLE) -> str:
60
        """
61
62
        :param word:
63
        :param style:
64
        :return:
65
        """
66
67 1
        def _word_origin_x(d_source_word, tag):
68 1
            w_origin_x = d_source_word.origin_x \
69
                if d_source_word.origin_x and d_source_word.type.group == "Cpx" else ''
70 1
            return tag % w_origin_x if w_origin_x else ''
71
72 1
        def _word_name(usage, d_source_word, tag):
73 1
            w_name = d_source_word.name if not usage \
74
                else usage.replace("%", d_source_word.name)
75 1
            return tag % w_name
76
77
        # de = definition english
78 1
        tags = {
79
            "normal": [
80
                '<span class="dg">(%s)</span>',
81
                '<span class="dt">[%s]</span> ',
82
                ' <span class="db">%s</span>',
83
                f'<span class="definition eng" id={self.id}>%s</span>',
84
                '<div class="d_line">%s</div>',
85
                '<span class="w_name">%s</span>, ',
86
                '<span class="w_origin">&lt;%s&gt;</span> ',
87
            ],
88
            "ultra": [
89
                '(%s)', '[%s] ', ' %s', '<de>%s</de>',
90
                '<ld>%s</ld>', '<wn>%s</wn>, ', '<o>&lt;%s&gt;</o> ', ],
91
        }
92 1
        t_d_gram, t_d_tags, t_d_body, t_def, t_def_line, t_word_name, t_word_origin = tags[style]
93
94 1
        gram_form = f'{str(self.slots) if self.slots else ""}' + self.grammar_code
95 1
        def_gram = t_d_gram % gram_form if gram_form else ''
96 1
        def_tags = t_d_tags % self.case_tags.replace("-", "&zwj;-&zwj;") if self.case_tags else ''
97
98 1
        def_body = HTMLExportDefinition.format_body(self.body)
99 1
        def_body = HTMLExportDefinition.highlight_key(def_body, word)
100 1
        def_body = t_d_body % def_body
101
102 1
        word_name = _word_name(self.usage, self.source_word, t_word_name)
103 1
        word_origin_x = _word_origin_x(self.source_word, t_word_origin)
104
105 1
        definition = t_def % f'{def_tags}{def_gram}{def_body}'
106 1
        return t_def_line % f'{word_name}{word_origin_x}{definition}'
107
108 1
    def export_for_loglan(self, style: str = DEFAULT_HTML_STYLE) -> str:
109
        """
110
111
        :param style:
112
        :return:
113
        """
114 1
        tags = {
115
            # usage, gram, body, tags, definition
116
            "normal": [
117
                '<span class="du">%s</span> ', '<span class="dg">(%s)</span> ',
118
                '<span class="db">%s</span>', ' <span class="dt">[%s]</span>',
119
                f'<div class="definition log" id={self.id}>%s</div>', ],
120
            "ultra": ['<du>%s</du> ', '(%s) ', '%s', ' [%s]', '<dl>%s</dl>', ],
121
        }
122 1
        t_d_usage, t_d_gram, t_d_body, t_d_tags, t_definition = tags[style]
123
124 1
        def_usage = t_d_usage % self.usage.replace("%", "—") if self.usage else ''
125 1
        gram_form = f"{str(self.slots) if self.slots else ''}" + self.grammar_code
126 1
        def_gram = t_d_gram % gram_form if gram_form else ''
127 1
        def_body = t_d_body % HTMLExportDefinition.format_body(self.body)
128 1
        def_tags = t_d_tags % self.case_tags.replace("-", "&zwj;-&zwj;") if self.case_tags else ''
129 1
        return t_definition % f'{def_usage}{def_gram}{def_body}{def_tags}'
130
131
132 1
class HTMLExportWord(ExportWord):
133
    """
134
    HTMLExportWord Class
135
    """
136
137 1
    @classmethod
138 1
    def html_all_by_name(
139
            cls, name: str, style: str = DEFAULT_HTML_STYLE,
140
            event_id: Union[BaseEvent, int, str] = None,
141
            case_sensitive: bool = False,
142
            partial_results: bool = False) -> Optional[str]:
143
        """
144
        Convert all words found by name into one HTML string
145
        Args:
146
            name: Name of the search word
147
            style: HTML design style
148
            event_id:
149
            case_sensitive:
150
            partial_results:
151
        Returns:
152
153
        """
154
155 1
        words_template = {
156
            "normal": '<div class="words">\n%s\n</div>\n',
157
            "ultra": '<ws>\n%s\n</ws>\n',
158
        }
159
160 1
        if not event_id:
161 1
            event_id = BaseEvent.latest().id
162
163 1
        event_id = int(event_id) if isinstance(event_id, (int, str)) else BaseEvent.id
164
165 1
        words = cls.query.filter(cls.event_start_id <= event_id) \
166
            .filter(or_(cls.event_end_id > event_id, cls.event_end_id.is_(None)))
167
168 1
        if case_sensitive:
169 1
            words = cls.__case_sensitive_words(name, words, partial_results)
170
        else:
171 1
            words = cls.__case_insensitive_words(name, words, partial_results)
172
173 1
        words = words.order_by(cls.name).all()
174
175 1
        if not words:
176 1
            return None
177
178 1
        items = cls._get_stylized_words(words, style)
179
180 1
        return words_template[style] % "\n".join(items)
181
182 1
    @classmethod
183
    def __case_insensitive_words(cls, name, words, partial_results):
184 1
        return words.filter(cls.name.ilike(f"{name}%")) \
185
            if partial_results else words.filter(cls.name.ilike(name))
186
187 1
    @classmethod
188
    def __case_sensitive_words(cls, name, words, partial_results):
189 1
        return words.filter(cls.name.like(f"{name}%")) \
190
            if partial_results else words.filter(cls.name == name)
191
192 1
    @staticmethod
193 1
    def _get_stylized_words(
194
            words: list, style: str = DEFAULT_HTML_STYLE) -> List[str]:
195
        """
196
197
        Args:
198
            words:
199
            style:
200
201
        Returns:
202
203
        """
204 1
        word_template = {
205
            "normal": '<div class="word" wid="%s">\n'
206
                      '<div class="word_line"><span class="word_name">%s</span>,</div>\n'
207
                      '<div class="meanings">\n%s\n</div>\n</div>',
208
            "ultra": '<w wid="%s"><wl>%s,</wl>\n<ms>\n%s\n</ms>\n</w>',
209
        }
210 1
        grouped_words = groupby(words, lambda ent: ent.name)
211 1
        group_words = {k: list(g) for k, g in grouped_words}
212 1
        items = []
213 1
        for word_name, words_list in group_words.items():
214 1
            meanings = "\n".join([word.html_meaning(style) for word in words_list])
215 1
            items.append(word_template[style] % (word_name.lower(), word_name, meanings))
216 1
        return items
217
218 1
    def html_origin(self, style: str = DEFAULT_HTML_STYLE):
219
        """
220
221
        Args:
222
            style:
223
224
        Returns:
225
226
        """
227 1
        orig = self.origin
228 1
        orig_x = self.origin_x
229
230 1
        if not (orig or orig_x):
231 1
            return str()
232
233 1
        origin = self.__generate_origin(orig, orig_x)
234
235 1
        if style == "normal":
236 1
            return f'<span class="m_origin">&lt;{origin}&gt;</span> '
237 1
        return f'<o>&lt;{origin}&gt;</o> '
238
239 1
    @staticmethod
240 1
    def __generate_origin(orig: str, orig_x: str) -> str:
241
        """
242
        Generate basic 'origin' string
243
        Args:
244
            orig:
245
            orig_x:
246
247
        Returns:
248
249
        """
250 1
        if not orig_x:
251 1
            return orig
252
253 1
        if not orig:
254 1
            return orig_x
255
256 1
        return f'{orig}={orig_x}'
257
258 1
    def html_definitions(self, style: str = DEFAULT_HTML_STYLE):
259
        """
260
261
        :param style:
262
        :return:
263
        """
264 1
        return [HTMLExportDefinition.export_for_loglan(
265
            d, style=style) for d in list(self.definitions)]
266
267 1
    def meaning(self, style: str = DEFAULT_HTML_STYLE) -> dict:
268
        """
269
270
        :param style:
271
        :return:
272
        """
273 1
        html_affixes, html_match, html_rank,\
274
            html_source, html_type, html_used_in,\
275
            html_year, t_technical = self.get_styled_values(style)
276
277 1
        html_tech = t_technical % f'{html_match}{html_type}{html_source}{html_year}{html_rank}'
278 1
        html_tech = f'{html_affixes}{self.html_origin(style)}{html_tech}'
279
280 1
        return {
281
            "mid": self.id,
282
            "technical": html_tech,
283
            "definitions": self.html_definitions(style),
284
            "used_in": html_used_in
285
        }
286
287 1
    def get_styled_values(self, style: str = DEFAULT_HTML_STYLE) -> tuple:
288
        """
289
290
        Args:
291
            style:
292
293
        Returns:
294
295
        """
296 1
        tags = {
297
            "normal": [
298
                '<span class="m_afx">%s</span> ', '<span class="m_match">%s</span> ',
299
                '<span class="m_rank">%s</span>', '<span class="m_author">%s</span> ',
300
                '<span class="m_type">%s</span> ', '<span class="m_use">%s</span>',
301
                '<span class="m_year">%s</span> ', '<span class="m_technical">%s</span>'],
302
            "ultra": [
303
                '<afx>%s</afx> ', '%s ', '%s', '%s ', '%s ',
304
                '<use>%s</use>', '%s ', '<tec>%s</tec>'],
305
        }
306
307 1
        def _stringer(tag: str, value: Optional[str], default_value: Optional[str] = str()):
308 1
            return tag % value if value else default_value
309
310
        # t_afx, t_match, t_type, t_author, t_year, t_rank, t_use, t_technical = tags[style]
311 1
        values = [self.e_affixes, self.match, self.rank, self.e_source, self.type.type,
312
                  self.e_usedin.replace("| ", "|&nbsp;"), self.e_year, None]
313 1
        default_values = [str(), str(), str(), str(), str(), None, str(), tags[style][-1]]
314
315 1
        return tuple([_stringer(tag, value, default_value) for tag, value, default_value in
316
                      zip(tags[style], values, default_values)])
317
318 1
    def html_meaning(self, style: str = DEFAULT_HTML_STYLE) -> str:
319
        """
320
321
        Args:
322
            style:
323
324
        Returns:
325
326
        """
327 1
        n_l = "\n"
328 1
        meaning_dict = self.meaning(style)
329 1
        if style == "normal":
330 1
            used_in_list = f'<div class="used_in">Used In: ' \
331
                           f'{meaning_dict.get("used_in")}</div>\n</div>' \
332
                if meaning_dict.get("used_in") else "</div>"
333 1
            return f'<div class="meaning" id="{meaning_dict.get("mid")}">\n' \
334
                   f'<div class="technical">{meaning_dict.get("technical")}</div>\n' \
335
                   f'<div class="definitions">{n_l}' \
336
                   f'{n_l.join(meaning_dict.get("definitions"))}\n</div>\n{used_in_list}'
337
338 1
        used_in_list = f'<us>Used In: {meaning_dict.get("used_in")}</us>\n</m>' \
339
            if meaning_dict.get("used_in") else '</m>'
340 1
        return f'<m>\n<t>{meaning_dict.get("technical")}</t>\n' \
341
               f'<ds>{n_l}' \
342
               f'{n_l.join(meaning_dict.get("definitions"))}\n</ds>\n{used_in_list}'
343
344 1
    @classmethod
345 1
    def translation_by_key(
346
            cls, key: str, language: str = None,
347
            style: str = DEFAULT_HTML_STYLE) -> Optional[str]:
348
        """
349
        Get information about loglan words by key in a foreign language
350
        Args:
351
            key:
352
            language:
353
            style:
354
355
        Returns:
356
357
        """
358
359 1
        words = cls.by_key(key, language).order_by(cls.name).all()
360
361 1
        if not words:
362 1
            return None
363
364 1
        result = cls.__definitions_by_key(key, words, style)
365
366 1
        new = '\n'
367
368 1
        return new.join([f"{new.join(definitions)}"
369
                         for _, definitions in result.items()]).strip()
370
371 1
    @classmethod
372
    def __definitions_by_key(cls, key, words, style):
373 1
        result = {}
374 1
        for word in words:
375 1
            result[word.name] = []
376 1
            definitions = [
377
                HTMLExportDefinition.export_for_english(d, word=key, style=style)
378
                for d in word.definitions if key.lower() in [key.word.lower() for key in d.keys]]
379 1
            result[word.name].extend(definitions)
380
        return result
381