Passed
Push — master ( f1bc36...dae6f1 )
by torrua
01:16
created

TelegramWord.format_origin()   A

Complexity

Conditions 4

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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