Passed
Push — master ( 653794...1bc565 )
by torrua
01:28
created

app.telegram_bot.bot.models   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 181
dl 0
loc 318
rs 9.92
c 0
b 0
f 0
wmc 31

12 Methods

Rating   Name   Duplication   Size   Complexity  
A TelegramWord.send_card_to_user() 0 11 1
B TelegramWord.export() 0 30 6
A TelegramWord._get_delimiter() 0 14 4
A TelegramWord._keyboard_navi() 0 39 3
A TelegramWord.translation_by_key() 0 32 2
A TelegramWord._keyboard_show() 0 18 1
A TelegramWord._keyboard_hide() 0 15 1
A TelegramWord._keyboard_data() 0 20 1
A TelegramDefinition.export() 0 22 3
A TelegramWord.get_definitions() 0 11 1
A TelegramWord.by_request() 0 12 2
B TelegramWord.keyboard_cpx() 0 37 5

1 Function

Rating   Name   Duplication   Size   Complexity  
A kb_close() 0 5 1
1
# -*- coding: utf-8 -*-
2
"""Model of LOD database for Telegram"""
3
4
from collections import defaultdict
5
from typing import List
6
7
from callbaker import callback_from_info
8
from keyboa import Keyboa
9
from loglan_core.addons.word_getter import AddonWordGetter
10
from loglan_core.connect_tables import t_connect_keys
11
from loglan_core.definition import BaseDefinition
12
from loglan_core.key import BaseKey
13
from loglan_core.word import BaseWord
14
15
from app.telegram_bot.bot import MIN_NUMBER_OF_BUTTONS
16
from app.telegram_bot.bot.variables import (
17
    t,
18
    cbd,
19
    mark_action,
20
    mark_entity,
21
    mark_record_id,
22
    mark_slice_start,
23
    action_predy_send_card,
24
    entity_predy,
25
    action_predy_kb_cpx_show,
26
    action_predy_kb_cpx_hide,
27
)
28
29
30
class TelegramDefinition(BaseDefinition):
31
    """Definition class extensions for Telegram"""
32
33
    def export(self):
34
        """
35
        Convert definition's data to str for sending as a telegram messages
36
        :return: Adopted for posting in telegram string
37
        """
38
        d_usage = f"<b>{self.usage.replace('%', '—')}</b> " if self.usage else ""
39
        d_grammar = (
40
            f"({self.slots if self.slots is not None else ''}{self.grammar_code}) "
41
        )
42
        d_body = (
43
            self.body.replace("<", "&#60;")
44
            .replace(">", "&#62;")
45
            .replace("«", "<i>")
46
            .replace("»", "</i>")
47
            .replace("{", "<code>")
48
            .replace("}", "</code>")
49
            .replace("....", "….")
50
            .replace("...", "…")
51
        )
52
53
        d_case_tags = f" [{self.case_tags}]" if self.case_tags else ""
54
        return f"{d_usage}{d_grammar}{d_body}{d_case_tags}"
55
56
57
class TelegramWord(BaseWord, AddonWordGetter):
58
    """Word class extensions for Telegram"""
59
60
    def export(self, session) -> str:
61
        """
62
        Convert word's data to str for sending as a telegram messages
63
        :return: List of str with technical info, definitions, used_in part
64
        """
65
66
        # Word
67
        list_of_afx = ["" + w.name for w in self.affixes]
68
        w_affixes = f" ({' '.join(list_of_afx)})" if list_of_afx else ""
69
        w_match = self.match + " " if self.match else ""
70
        w_year = "'" + str(self.year.year)[-2:] + " "
71
        w_origin_x = " = " + self.origin_x if self.origin_x else ""
72
        w_orig = (
73
            "\n<i>&#60;" + self.origin + w_origin_x + "&#62;</i>"
74
            if self.origin or w_origin_x
75
            else ""
76
        )
77
        w_authors = "/".join([a.abbreviation for a in self.authors]) + " "
78
        w_type = self.type.type + " "
79
        w_rank = self.rank + " " if self.rank else ""
80
        word_str = (
81
            f"<b>{self.name}</b>{w_affixes},"
82
            f"\n{w_match}{w_type}{w_authors}{w_year}{w_rank}{w_orig}"
83
        )
84
85
        # Definitions TODO maybe extract from method
86
        definitions_str = "\n\n".join(
87
            [d.export() for d in self.get_definitions(session=session)]
88
        )
89
        return f"{word_str}\n\n{definitions_str}"
90
91
    def get_definitions(self, session) -> List[TelegramDefinition]:
92
        """
93
        Get all definitions of the word
94
        :param session: Session
95
        :return: List of Definition objects ordered by position
96
        """
97
        return (
98
            session.query(TelegramDefinition)
99
            .filter(BaseDefinition.word_id == self.id)
100
            .order_by(BaseDefinition.position.asc())
101
            .all()
102
        )
103
104
    @classmethod
105
    def translation_by_key(cls, session, request: str, language: str = None) -> str:
106
        """
107
        We get information about loglan words by key in a foreign language
108
        :param session: Session
109
        :param request: Requested string
110
        :param language: Key language
111
        :return: Search results string formatted for sending to Telegram
112
        """
113
        words = (
114
            session.query(cls.name, TelegramDefinition)
115
            .join(BaseDefinition)
116
            .join(t_connect_keys)
117
            .join(BaseKey)
118
            .filter(BaseKey.word == request)
119
            .filter(BaseKey.language == language)
120
            .order_by(cls.id, BaseDefinition.position)
121
            .all()
122
        )
123
124
        result = defaultdict(list)
125
126
        for word in words:
127
            name, definition = word
128
            result[name].append(definition.export())
129
130
        new = "\n"
131
        word_items = [
132
            f"/{word_name},\n{new.join(definitions)}\n"
133
            for word_name, definitions in result.items()
134
        ]
135
        return new.join(word_items).strip()
136
137
    def _keyboard_navi(self, index_start: int, index_end: int, delimiter: int):
138
        """
139
        :param index_start:
140
        :param index_end:
141
        :param delimiter:
142
        :return:
143
        """
144
        text_arrow_back = "\U0000276E" * 2
145
        text_arrow_forward = "\U0000276F" * 2
146
        button_back, button_forward = None, None
147
148
        common_data = {
149
            mark_entity: entity_predy,
150
            mark_action: action_predy_kb_cpx_show,
151
            mark_record_id: self.id,
152
        }
153
154
        if index_start != 0:
155
            cbd_predy_kb_cpx_back = {
156
                **common_data,
157
                mark_slice_start: index_start - delimiter,
158
            }
159
            button_back = {
160
                t: text_arrow_back,
161
                cbd: callback_from_info(cbd_predy_kb_cpx_back),
162
            }
163
164
        if index_end != len(self.complexes):
165
            cbd_predy_kb_cpx_forward = {
166
                **common_data,
167
                mark_slice_start: index_end,
168
            }
169
            button_forward = {
170
                t: text_arrow_forward,
171
                cbd: callback_from_info(cbd_predy_kb_cpx_forward),
172
            }
173
174
        nav_row = [b for b in [button_back, button_forward] if b]
175
        return Keyboa(nav_row, items_in_row=2)()
176
177
    def _keyboard_hide(self, total_number_of_complexes: int):
178
        """
179
        :param total_number_of_complexes:
180
        :return:
181
        """
182
        text_cpx_hide = f"Hide Complex{'es' if total_number_of_complexes > 1 else ''}"
183
        cbd_predy_kb_cpx_hide = {
184
            mark_entity: entity_predy,
185
            mark_action: action_predy_kb_cpx_hide,
186
            mark_record_id: self.id,
187
        }
188
        button_predy_kb_cpx_hide = [
189
            {t: text_cpx_hide, cbd: callback_from_info(cbd_predy_kb_cpx_hide)},
190
        ]
191
        return Keyboa(button_predy_kb_cpx_hide)()
192
193
    def _keyboard_show(self, total_number_of_complexes: int):
194
        """
195
        :param total_number_of_complexes:
196
        :return:
197
        """
198
        text_cpx_show = (
199
            f"Show Complex{'es' if total_number_of_complexes > 1 else ''}"
200
            f" ({total_number_of_complexes})"
201
        )
202
        cbd_predy_kb_cpx_show = {
203
            mark_entity: entity_predy,
204
            mark_action: action_predy_kb_cpx_show,
205
            mark_record_id: self.id,
206
        }
207
        button_show = [
208
            {t: text_cpx_show, cbd: callback_from_info(cbd_predy_kb_cpx_show)},
209
        ]
210
        return Keyboa.combine((Keyboa(button_show)(), kb_close()))
211
212
    @staticmethod
213
    def _get_delimiter(total_number_of_complexes: int):
214
        """
215
        :param total_number_of_complexes:
216
        :return:
217
        """
218
        allowed_range = list(range(MIN_NUMBER_OF_BUTTONS, MIN_NUMBER_OF_BUTTONS + 11))
219
        lst = [(total_number_of_complexes % i, i) for i in allowed_range]
220
        delimiter = min(lst, key=lambda x: abs(x[0] - MIN_NUMBER_OF_BUTTONS))[1]
221
        for i in lst:
222
            if i[0] == 0:
223
                delimiter = i[1]
224
                break
225
        return delimiter
226
227
    @staticmethod
228
    def _keyboard_data(current_complexes: list):
229
        """
230
        :param current_complexes:
231
        :return:
232
        """
233
        cpx_items = [
234
            {
235
                t: cpx.name,
236
                cbd: callback_from_info(
237
                    {
238
                        mark_entity: entity_predy,
239
                        mark_action: action_predy_send_card,
240
                        mark_record_id: cpx.id,
241
                    }
242
                ),
243
            }
244
            for cpx in current_complexes
245
        ]
246
        return Keyboa(items=cpx_items, alignment=True, items_in_row=4)()
247
248
    def keyboard_cpx(self, show_list: bool = False, slice_start: int = 0):
249
        """
250
        :param show_list:
251
        :param slice_start:
252
        :return:
253
        """
254
255
        total_num_of_cpx = len(self.complexes)
256
257
        if not total_num_of_cpx:
258
            return kb_close()
259
260
        if not show_list:
261
            return self._keyboard_show(total_num_of_cpx)
262
263
        current_delimiter = self._get_delimiter(total_num_of_cpx)
264
265
        kb_cpx_hide = self._keyboard_hide(total_num_of_cpx)
266
267
        last_allowed_item = slice_start + current_delimiter
268
        slice_end = (
269
            last_allowed_item
270
            if last_allowed_item < total_num_of_cpx
271
            else total_num_of_cpx
272
        )
273
274
        current_cpx_set = self.complexes[slice_start:slice_end]
275
        kb_cpx_data = self._keyboard_data(current_cpx_set)
276
277
        kb_cpx_nav = None
278
279
        if total_num_of_cpx > current_delimiter:
280
            kb_cpx_nav = self._keyboard_navi(slice_start, slice_end, current_delimiter)
281
282
        kb_combo = (kb_cpx_hide, kb_cpx_data, kb_cpx_nav, kb_close())
283
284
        return Keyboa.combine(kb_combo)
285
286
    def send_card_to_user(self, session, bot, user_id: int | str):
287
        """
288
        :param session:
289
        :param bot:
290
        :param user_id:
291
        :return:
292
        """
293
        bot.send_message(
294
            chat_id=user_id,
295
            text=self.export(session),
296
            reply_markup=self.keyboard_cpx()
297
        )
298
299
    @classmethod
300
    def by_request(cls, session, request: str) -> list:
301
        """
302
        :param session:
303
        :param request:
304
        :return:
305
        """
306
        if isinstance(request, int):
307
            return [
308
                cls.get_by_id(session, request),
309
            ]
310
        return cls.by_name(session, request).all()
311
312
313
def kb_close():
314
    """
315
    :return:
316
    """
317
    return Keyboa({t: "Close", cbd: "close"})()
318