Commit 0fd79827789847350a22a18ec18d78471d7bc0cf

Authored by Василь Свідрик
1 parent 282a39ee6f
Exists in master

add k2, k2obj class, other

Showing 38 changed files with 618 additions and 464 deletions Inline Diff

__pycache__/main.cpython-310.pyc
No preview for this file type
__pycache__/routes.cpython-310.pyc
No preview for this file type
components/k2test/.idea/.gitignore
File was created 1 # Default ignored files
2 /shelf/
3 /workspace.xml
4
components/k2test/.idea/gittest.iml
File was created 1 <?xml version="1.0" encoding="UTF-8"?>
2 <module type="PYTHON_MODULE" version="4">
3 <component name="NewModuleRootManager">
4 <content url="file://$MODULE_DIR$">
5 <excludeFolder url="file://$MODULE_DIR$/venv" />
6 </content>
7 <orderEntry type="inheritedJdk" />
8 <orderEntry type="sourceFolder" forTests="false" />
9 </component>
10 </module>
components/k2test/.idea/inspectionProfiles/profiles_settings.xml
File was created 1 <component name="InspectionProjectProfileManager">
2 <settings>
3 <option name="USE_PROJECT_PROFILE" value="false" />
4 <version value="1.0" />
5 </settings>
6 </component>
components/k2test/.idea/misc.xml
File was created 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4">
3 <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (gittest)" project-jdk-type="Python SDK" />
4 <component name="PyCharmProfessionalAdvertiser">
5 <option name="shown" value="true" />
6 </component>
7 </project>
components/k2test/.idea/modules.xml
File was created 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4">
3 <component name="ProjectModuleManager">
4 <modules>
5 <module fileurl="file://$PROJECT_DIR$/.idea/gittest.iml" filepath="$PROJECT_DIR$/.idea/gittest.iml" />
6 </modules>
7 </component>
8 </project>
components/k2test/.idea/vcs.xml
File was created 1 <?xml version="1.0" encoding="UTF-8"?>
2 <project version="4">
3 <component name="VcsDirectoryMappings">
4 <mapping directory="$PROJECT_DIR$" vcs="Git" />
5 </component>
6 </project>
components/k2test/__pycache__/__init__.cpython-310.pyc
No preview for this file type
components/k2test/k2test/__pycache__/__init__.cpython-310.pyc
No preview for this file type
components/k2test/k2test/__pycache__/views.cpython-310.pyc
No preview for this file type
components/k2test/k2test/languages/en/LC_MESSAGES/messages.mo
No preview for this file type
components/k2test/k2test/languages/en/LC_MESSAGES/messages.po
File was created 1 msgid "Installed components"
2 msgstr "Installed components"
3
4 msgid "Name"
5 msgstr "Name"
6
7 msgid "Description"
8 msgstr "Description"
9
10 msgid "Version"
11 msgstr "Version"
12
13 msgid "Dependencies"
14 msgstr "Dependencies"
15
16 msgid "Git Link"
17 msgstr "Git Link"
18
19 msgid "Install At"
20 msgstr "Install At"
21
22 msgid "Status"
23 msgstr "Status"
24
25 msgid "Action"
26 msgstr "Action"
27
28 msgid "Components Shop"
29 msgstr "Components Shop"
30
31 msgid "New 2 page"
32 msgstr "New 2 page"
33
components/k2test/k2test/languages/uk/LC_MESSAGES/messages.mo
No preview for this file type
components/k2test/k2test/languages/uk/LC_MESSAGES/messages.po
File was created 1 msgid "New 2 page"
2 msgstr "Нова сторінка 2"
3
components/k2test/k2test/templates/login.html
File was created 1 login
2
components/k2test/k2test/templates/new2.html
File was created 1
2
3 <h2>{{ _('New 2 page') }}</h2>
components/k2test/k2test/views.py
File was created 1 from flask import render_template, redirect, request
2 from flask_login import login_user, current_user, logout_user, login_required
3 from flask import current_app as k2
4 from flask import Blueprint, json
5 from flask_sqlalchemy import SQLAlchemy
6
7
8
9 k2test = Blueprint('k2test', __name__, template_folder='templates', static_folder='static',)
10
11
12
13 @k2test.route('/new')
14 def new_component():
15 db = k2.extensions['sqlalchemy'].db
16 connection = db.engine.connect()
17 query = db.text("SELECT email, login FROM k2users")
18 result = connection.execute(query).fetchall()
19 rows = [dict(email=row.email, login=row.login) for row in result]
20 # Перетворення результату на JSON
21 json_result = json.dumps(rows)
22 return json_result
23
24 @k2test.route('/new2')
25 def new_component2():
26 return render_template('new2.html')
27
28 @k2test.route('/new3')
29 def new_component3():
30 return 'сторінка 3'
31
32 @k2test.route('/new4')
33 def new_component4():
34 return 'сторінка 4'
35
components/k2test/requirements.txt
File was created 1 Flask-Login==0.6.2
2
db/__pycache__/database.cpython-310.pyc
No preview for this file type
No preview for this file type
db/database.py
1 from flask_sqlalchemy import SQLAlchemy File was deleted
2 from sqlalchemy import text
3 import yaml
4 import os
5 # load database settings from yml file
6 def init_db_uri():
7 with open('db/db.yml', 'r') as f:
8 db_config = yaml.load(f, Loader=yaml.FullLoader)
9 return db_config
10 # creating a connection string
11 db = SQLAlchemy()
12
13 def init_db(app):
14 db_config = init_db_uri()
15 db_uri = f"mysql+{db_config['driver']}://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['dbname']}"
16 app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
17 basedir = os.path.abspath(os.path.dirname(__file__))
18 db_path = os.path.join(basedir, '', '', 'database.db')
19 app.config['SQLALCHEMY_BINDS'] = {'db2': f'sqlite:///{db_path}'}
20 db.init_app(app)
21 1 from flask_sqlalchemy import SQLAlchemy
22 2 from sqlalchemy import text
23 3 import yaml
24 4 import os
k2/__pycache__/k2cfg.cpython-310.pyc
No preview for this file type
k2/__pycache__/k2inst.cpython-310.pyc
No preview for this file type
k2/__pycache__/k2obj.cpython-310.pyc
No preview for this file type
k2/__pycache__/root.cpython-310.pyc
No preview for this file type
File was created 1 from flask_sqlalchemy import SQLAlchemy
2 from sqlalchemy import text
3 from .k2obj import k2obj
4 import yaml
5 import os
6 import requests
7 import pytz
8 import os
9 import sys
10 from flask import session
11
12 class k2(k2obj):
13
14 version = '2.5.1.27'
15 db = SQLAlchemy()
16 domain = 'http://127.0.0.1:5000/'
17 update_domain ='http://127.0.0.1:8001/'
18 venv_bin_path = os.path.join(os.path.dirname(sys.prefix), 'venv/Scripts')
19 json_sort_keys = False
20 timezone = pytz.timezone('Europe/Kiev')
21 secret_key = 'JH&INH987gFDHdsagh&8dwbjdw8ckw'
22 default_language = 'en'
23 current_language = ''
24 babel_translation_directories = ''
25
26 def __init__(self):
27 super().__init__()
28
29 @classmethod
30 def load_babel_translation_directories(cls):
31 with open('languages/babel_translation_directories.yml', 'r') as f:
32 data = yaml.safe_load(f)
33 cls.babel_translation_directories = data['babel_translation_directories']
34
35 @classmethod
36 def get_locale(cls):
37 if 'lang' in session:
38 # Отримати обрану мову з сеансу користувача
39 return session['lang']
40 else:
41 # Повернути мову за замовчуванням з налаштувань додатку
42 return cls.default_language #app.config['DEFAULT_LANGUAGE']
43
44 @classmethod
45 def search_babel_translation_directories(cls):
46 response = requests.get(f'{cls.domain}api/languages')
47 if response.status_code == 200:
48 cls.babel_translation_directories = response.text
49 data = {'babel_translation_directories': response.text}
50 else:
51 cls.babel_translation_directories = 'languages'
52 data = {'babel_translation_directories': 'languages'}
53
54 with open('languages/babel_translation_directories.yml', 'w') as f:
55 yaml.dump(data, f)
56
57 # load database settings from yml file
58 def init_db_uri(self):
59 with open('db/db.yml', 'r') as f:
60 db_config = yaml.load(f, Loader=yaml.FullLoader)
61 return db_config
62
63 # creating a connection string
64
65
66 def init_db(self, app):
67 db_config = self.init_db_uri()
68 db_uri = f"mysql+{db_config['driver']}://{db_config['user']}:{db_config['password']}@{db_config['host']}/{db_config['dbname']}"
69 app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
70 db_path = '../db/database.db'
71 app.config['SQLALCHEMY_BINDS'] = {'db2': f'sqlite:///{db_path}'}
72 self.db.init_app(app)
73
74
75
76
77
78
File was created 1 import zipfile
2 from flask import Flask, Blueprint, jsonify, render_template, redirect, url_for, session, g
3 import os
4 import shutil
5 from datetime import datetime
6 import uuid
7 from flask import request
8 from k2.k2cfg import k2
9 from flask_migrate import Migrate
10 import requests
11 import subprocess
12 from flask import current_app
13 import yaml
14 from functools import wraps
15
16 db = k2.db
17 #migrate = Migrate(app, db)
18 def generate_id():
19 generated_id = uuid.uuid4().hex[:32]
20 return ''.join([generated_id[i:i + 4] for i in range(0, len(generated_id), 4)])
21 class Component(db.Model):
22 __bind_key__ = 'db2'
23 __tablename__ = 'component'
24 __table_args__ = {'extend_existing': True}
25 id = db.Column(db.String(36), primary_key=True, default=generate_id)
26 name = db.Column(db.String(100), nullable=False)
27 description = db.Column(db.String(200))
28 version = db.Column(db.String(20))
29 git_link = db.Column(db.String(200))
30 dependencies = db.Column(db.String)
31 date_installed = db.Column(db.DateTime, default=datetime.utcnow)
32 installed = db.Column(db.Boolean, default=False)
33
34 def __init__(self, name, description, version, git_link, dependencies, installed):
35 self.name = name
36 self.description = description
37 self.version = version
38 self.git_link = git_link
39 self.dependencies = dependencies
40 self.installed = installed
41 def __repr__(self):
42 return f"Component('{self.name}', '{self.version}')"
43 @classmethod
44 def get_repository_by_id(self,rep_id):
45 component = db.session.query(Component).filter(Component.id == rep_id).first()
46 return component
47 components_bp = Blueprint('components', __name__, template_folder='templates')
48 config = yaml.safe_load(open("db/first-login.yml"))
49
50 def first_login_required(view_func):
51 @wraps(view_func)
52 def decorated_view(*args, **kwargs):
53 if 'username' not in session:
54 return redirect(url_for('first_login'))
55 return view_func(*args, **kwargs)
56
57 return decorated_view
58
59
60 @components_bp.route('/api/languages', methods=['GET'])
61 def find_language():
62 components = Component.query.all()
63 languages_paths = []
64 # Додаємо головну директорію languages
65 languages_paths.append('languages')
66 # Знаходимо шляхи до директорій languages всередині папки components
67 for component in components:
68 component_languages_directory = 'components/' + component.name + '/' + component.name + '/languages'
69 languages_paths.append(component_languages_directory)
70 result = ';'.join(languages_paths)
71 print(result)
72 return result
73
74 @components_bp.route('/', methods=['GET', 'POST'])
75 @first_login_required
76 def home():
77 return redirect('/dashboard')
78
79 @components_bp.route('/login', methods=['GET', 'POST'])
80 def first_login():
81 if request.method == "POST":
82 username = request.form.get("username")
83 password = request.form.get("password")
84 for user in config["users"]:
85 if user["username"] == username and user["password"] == password:
86 session['username'] = username # Збереження ім'я користувача в сесії
87 return redirect(url_for('components.dashboard'))
88 return "Невірне ім'я користувача або пароль."
89 return render_template('first-login.html')
90
91 @components_bp.route('/logout')
92 def logout():
93 session.pop('username', None) # Видалення ім'я користувача з сесії
94 return redirect(url_for('first_login'))
95
96 @components_bp.route('/change_language/<lang>')
97 def change_language(lang):
98 session['lang'] = lang
99 k2.current_language = lang
100 return redirect(url_for('components.dashboard'))
101
102 @components_bp.route('/dashboard')
103 @first_login_required
104 def dashboard():
105
106 # Компоненти доступні для встановлення
107 try:
108 # GET-запит до API
109 response = requests.get('http://127.0.0.1:8001/api/components')
110 json_data = response.json()
111 # Перетворення JSON-об'єкту на масив
112 component_server = [item for item in json_data]
113 except:
114 component_server = None
115
116 # Встановлені компоненти
117 components = Component.query.all()
118 components_names = [component.name for component in components]
119 # Отримання значення пошукового запиту з параметрів URL
120 search_query = request.args.get('search_query')
121 #print(search_query)
122 filtered_components = []
123 if search_query:
124 filtered_components = [component for component in component_server if
125 (search_query.lower() in component['name'].lower() if component['name'] else False) or
126 (search_query.lower() in component['description'].lower() if component[
127 'description'] else False)]
128 else:
129 filtered_components = component_server
130 current_language = k2.current_language
131
132 return render_template('dashboard.html', components=components,
133 components_names=components_names, component_server=filtered_components,
134 search_query=search_query, language=current_language )
135
136 @components_bp.route('/show_components/<string:component_id>')
137 @first_login_required
138 def show_components(component_id):
139 response = requests.get('http://127.0.0.1:8001/api/components')
140 json_data = response.json()
141 selected_component = next((component for component in json_data if component['id'] == component_id), None)
142 return render_template('component-info.html', selected_component=selected_component)
143
144 @components_bp.route('/install_components_git/<string:component_id>')
145 @first_login_required
146 def install_components_git(component_id):
147 # Шлях до головної папки проекту
148 project_folder = 'components'
149 #component = Component.get_repository_by_id(component_id)
150 response = requests.get('http://127.0.0.1:8001/api/components')
151 json_data = response.json()
152 selected_component = next((component for component in json_data if component['id'] == component_id), None)
153 # Назва репозиторія
154 repository_name = selected_component['name']
155 # Шлях до папки репозиторія в межах проекту
156 repository_folder = os.path.join(project_folder, repository_name)
157 # URL репозиторія
158
159 git_repo_url = selected_component['git_link']
160 try:
161 # Перевірка наявності папки репозиторія
162 if not os.path.exists(repository_folder):
163 # Створення папки репозиторія
164 os.makedirs(repository_folder)
165 # Шлях до файлу __init__.py
166 init_file = os.path.join(repository_folder, '__init__.py')
167 # Перевірка наявності файлу __init__.py
168 if not os.path.exists(init_file):
169 # Створення пустого файлу __init__.py
170 open(init_file, 'a').close()
171 #підключення до приватного репозиторію
172 #os.environ['GITLAB_PRIVATE_TOKEN'] = '8xxTpxrKVDGoXD5ynjiW'
173 #git_repo_url_with_token = git_repo_url.replace('https://',
174 # f'https://oauth2:{os.environ["GITLAB_PRIVATE_TOKEN"]}@')
175
176 # Команда для встановлення з використанням git_repo_url і повного шляху до папки репозиторія
177 command = ['venv/Scripts/python.exe', '-m', 'pip', 'install', '--use-pep517', 'git+' + git_repo_url, '--target=' + repository_folder]
178 # Виконуємо команду встановлення
179 subprocess.check_call(command)
180
181 # Шлях до файлу, до якого потрібно додати код
182 file_path = 'routes.py'
183 # Код, який потрібно додати
184 code = selected_component['dependencies']
185 # Відкриття файлу у режимі дозапису
186 with open(file_path, 'a') as file:
187 # Запис нового коду у файл
188 file.write('\n')
189 file.write(code)
190 file.write('\n')
191 component = Component.query.filter_by(name=selected_component['name']).first()
192 if not component:
193 new_component = Component(
194 name=selected_component['name'],
195 description=selected_component['description'],
196 version=selected_component['version'],
197 git_link=selected_component['git_link'],
198 dependencies=selected_component['dependencies'],
199 installed=True
200 )
201 # Add the new component to the database
202 db.session.add(new_component)
203 db.session.commit()
204
205 # Повертаємо повідомлення про успішне встановлення
206 return f'Installed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
207 except subprocess.CalledProcessError as e:
208 return 'Error installing : ' + str(e)
209
210 @components_bp.route('/install_components/<string:component_id>')
211 @first_login_required
212 def install_component_from_archive(component_id):
213 # Шлях до головної папки проекту
214 project_folder = 'components'
215 # Отримати відповідну компоненту зі списку компонент
216 response = requests.get('http://127.0.0.1:8001/api/components')
217 json_data = response.json()
218 selected_component = next((component for component in json_data if component['id'] == component_id), None)
219 if selected_component is None:
220 return 'Component not found'
221 # Отримати посилання на архів компоненти та версію
222 archive_url = selected_component['latest_component_data']
223 version = selected_component['latest_version']
224 try:
225 # Створити шлях до папки компоненти згідно назви та версії
226 component_folder = os.path.join(project_folder)
227 os.makedirs(component_folder, exist_ok=True)
228 # Завантажити архів компоненти
229 response = requests.get(archive_url, stream=True)
230 response.raise_for_status()
231 # Шлях до завантаженого архіву
232 archive_path = os.path.join(component_folder, f"{selected_component['name']}.zip")
233 # Встановити залежності з файлу requirements.txt
234
235 # Зберегти архів на диск
236 with open(archive_path, "wb") as file:
237 for chunk in response.iter_content(chunk_size=8192):
238 file.write(chunk)
239 # Розпакувати архів
240 with zipfile.ZipFile(archive_path, "r") as zip_ref:
241 zip_ref.extractall(component_folder)
242
243 # Видалити архів
244 os.remove(archive_path)
245
246 # Шлях до файлу, до якого потрібно додати код
247 file_path = 'routes.py'
248 # Код, який потрібно додати
249 code = f'''from components.{selected_component['name']}.{selected_component['name']}.views import {selected_component['name']}\napp.register_blueprint({selected_component['name']}, url_prefix='/{selected_component['name']}')'''
250 #selected_component['dependencies']
251
252 # Відкриття файлу у режимі дозапису
253 with open(file_path, 'a') as file:
254 # Запис нового коду у файл
255 file.write('\n')
256 file.write(code)
257 file.write('\n')
258
259 # Оновити базу даних з встановленою компонентою
260 component = Component.query.filter_by(name=selected_component['name']).first()
261 if not component:
262 component = Component(
263 name=selected_component['name'],
264 description=selected_component['description'],
265 version=version,
266 git_link=selected_component['git_link'],
267 dependencies=code,
268 installed=True
269 )
270 db.session.add(component)
271 else:
272 component.version = version
273 component.git_link = selected_component['git_link']
274 component.dependencies = code
275 component.installed = True
276
277 db.session.commit()
278
279
280 k2.search_babel_translation_directories()
281
282 return f'''Component installed successfully: {selected_component["name"]} v{version} {k2.babel_translation_directories},
283 \n \n please wait installing requirments...
284 <meta http-equiv="refresh" content="0;url=/install-requirments/{selected_component["name"]}" />'''
285
286 except Exception as e:
287 return f'Error installing component: {str(e)}'
288
289 @components_bp.route('/install-requirments/<string:selected_component_name>', methods=['GET'])
290 @first_login_required
291 def install_requirments(selected_component_name):
292 requirements_file = os.path.join('components', selected_component_name, "requirements.txt")
293 if os.path.isfile(requirements_file):
294 pip_command = f"{k2.venv_bin_path}/python -m pip install -r {requirements_file}"
295 subprocess.run(pip_command, shell=True, check=True)
296 return f'''Requirements installed successfully
297 \n \n please wait installing requirments components...
298 <meta http-equiv="refresh" content="1;url=/install-requirments-components" />'''
299
300
301 @components_bp.route('/install-requirments-components')
302 @first_login_required
303 def install_requirements_components():
304 # Откриття файлу requirements_components.txt
305 requirements_file = 'requirements_components.txt'
306 component_ids = None
307 if os.path.isfile(requirements_file):
308 with open('requirements_components.txt', 'r') as file:
309 component_ids = file.read().splitlines()
310 if component_ids:
311 for component_id in component_ids:
312 # Виклик роуту '/install_components/<string:component_name>' для кожної назви компоненти
313 response = requests.get(
314 f'/install_components/{component_id}')
315 return f'Requirements components installed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
316 else:
317 return f'<meta http-equiv="refresh" content="1;url=/dashboard" />'
318 # Опрацювання відповіді (за потреби)
319
320
321 @components_bp.route('/remove_dependencies/<string:component_id>', methods=['GET'])
322 @first_login_required
323 def remove_dependencies(component_id):
324 # Знаходимо компоненту за її ID
325 component = Component.query.get(component_id)
326 if not component:
327 return 'Component not found <meta http-equiv="refresh" content="1;url=/dashboard" />'
328
329 # Шлях до файлу, з якого потрібно видалити залежності
330 file_path = 'routes.py'
331
332 # Зчитуємо вміст файлу
333 with open(file_path, 'r') as file:
334 lines = file.readlines()
335
336 # Видаляємо рядки, що містять залежності компоненти
337 updated_lines = [line for line in lines if line.strip() not in component.dependencies]
338
339 # Записуємо оновлений вміст у файл
340 with open(file_path, 'w') as file:
341 file.writelines(updated_lines)
342
343 # Оновлюємо статус компоненти
344 component.installed = False
345 db.session.commit()
346
347 return 'Component successfully turn off <meta http-equiv="refresh" content="1;url=/dashboard" />'
348
349 @components_bp.route('/add_dependencies/<string:component_id>', methods=['GET'])
350 @first_login_required
351 def add_dependencies(component_id):
352
353 # Знаходимо компоненту за її ID
354 component = Component.query.get(component_id)
355 if not component:
356 return 'Component not found <meta http-equiv="refresh" content="1;url=/dashboard" />'
357
358 # Шлях до файлу, з якого потрібно видалити залежності
359 file_path = 'routes.py'
360 code = component.dependencies
361 # Відкриття файлу у режимі дозапису
362 with open(file_path, 'a') as file:
363 # Запис нового коду у файл
364 file.write(code)
365
366 # Оновлюємо статус компоненти
367 component.installed = True
368 db.session.commit()
369 return 'Dependencies added successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
370
371 @components_bp.route('/remove-component/<string:component_id>')
372 @first_login_required
373 def remove_component(component_id):
374 # Шлях до головної папки проекту
375 project_folder = 'components'
376 component = Component.query.get(component_id)
377 # Назва репозиторія
378 repository_name = component.name
379 # Шлях до папки репозиторія в межах проекту
380 repository_folder = os.path.join(project_folder, repository_name)
381 try:
382 # Перевірка наявності папки репозиторія
383 if os.path.exists(repository_folder):
384 # Видалення папки репозиторія
385 shutil.rmtree(repository_folder)
386 # Видаляємо компоненту з бази
387 if component:
388 db.session.delete(component)
389 db.session.commit()
390
391 # Повертаємо повідомлення про успішне видалення
392 return 'Component removed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
393 except Exception as e:
394 # Повертаємо повідомлення про помилку видалення
395 return 'Error removing component: ' + str(e)
396
File was created 1 class k2obj:
2 name = ''
3 version = ''
4 domain = ''
5 timezone = ''
6 menu = ''
7
8
9
1 import zipfile File was deleted
2 from flask import Flask, Blueprint, jsonify, render_template, redirect, url_for, session, g
3 import os
4 import shutil
5 from datetime import datetime
6 import uuid
7 from flask import request
8 from db.database import db
9 from flask_migrate import Migrate
10 from main import app
11 import requests
12 import subprocess
13 from flask import current_app
14 import yaml
15 from functools import wraps
16 from languages.views import get_locale
17
18
19 with app.app_context():
20
21 #migrate = Migrate(app, db)
22 def generate_id():
23 generated_id = uuid.uuid4().hex[:32]
24 return ''.join([generated_id[i:i + 4] for i in range(0, len(generated_id), 4)])
25 class Component(db.Model):
26 __bind_key__ = 'db2'
27 __tablename__ = 'component'
28 __table_args__ = {'extend_existing': True}
29 id = db.Column(db.String(36), primary_key=True, default=generate_id)
30 name = db.Column(db.String(100), nullable=False)
31 description = db.Column(db.String(200))
32 version = db.Column(db.String(20))
33 git_link = db.Column(db.String(200))
34 dependencies = db.Column(db.String)
35 date_installed = db.Column(db.DateTime, default=datetime.utcnow)
36 installed = db.Column(db.Boolean, default=False)
37
38 def __init__(self, name, description, version, git_link, dependencies, installed):
39 self.name = name
40 self.description = description
41 self.version = version
42 self.git_link = git_link
43 self.dependencies = dependencies
44 self.installed = installed
45 def __repr__(self):
46 return f"Component('{self.name}', '{self.version}')"
47 @classmethod
48 def get_repository_by_id(self,rep_id):
49 component = db.session.query(Component).filter(Component.id == rep_id).first()
50 return component
51 components_bp = Blueprint('components', __name__, template_folder='templates')
52 config = yaml.safe_load(open("db/first-login.yml"))
53
54
55 def first_login_required(view_func):
56 @wraps(view_func)
57 def decorated_view(*args, **kwargs):
58 if 'username' not in session:
59 return redirect(url_for('first_login'))
60 return view_func(*args, **kwargs)
61
62 return decorated_view
63
64
65 def find_language():
66 components = Component.query.all()
67 languages_paths = []
68 # Додаємо головну директорію languages
69 languages_paths.append('languages')
70 # Знаходимо шляхи до директорій languages всередині папки components
71 for component in components:
72 component_languages_directory = 'components/' + component.name + '/languages'
73 languages_paths.append(component_languages_directory)
74 result = ';'.join(languages_paths)
75 return result
76
77 @app.route('/', methods=['GET', 'POST'])
78 @first_login_required
79 def home():
80 return redirect('/dashboard')
81
82
83 @app.route('/login', methods=['GET', 'POST'])
84 def first_login():
85 if request.method == "POST":
86 username = request.form.get("username")
87 password = request.form.get("password")
88 for user in config["users"]:
89 if user["username"] == username and user["password"] == password:
90 session['username'] = username # Збереження ім'я користувача в сесії
91 return redirect(url_for('components.dashboard'))
92 return "Невірне ім'я користувача або пароль."
93 return render_template('first-login.html')
94
95
96 @app.route('/logout')
97 def logout():
98 session.pop('username', None) # Видалення ім'я користувача з сесії
99 return redirect(url_for('login'))
100
101
102 @components_bp.route('/change_language/<lang>')
103 def change_language(lang):
104 session['lang'] = lang
105 app.config['CURRENT_LANGUAGE'] = lang
106 return redirect(url_for('components.dashboard'))
107
108 @components_bp.route('/dashboard')
109 @first_login_required
110 def dashboard():
111
112 # Компоненти доступні для встановлення
113 try:
114 # GET-запит до API
115 response = requests.get('http://127.0.0.1:8001/api/components')
116 json_data = response.json()
117 # Перетворення JSON-об'єкту на масив
118 component_server = [item for item in json_data]
119 except:
120 component_server = None
121
122 # Встановлені компоненти
123 components = Component.query.all()
124 components_names = [component.name for component in components]
125 # Отримання значення пошукового запиту з параметрів URL
126 search_query = request.args.get('search_query')
127 #print(search_query)
128 filtered_components = []
129 if search_query:
130 filtered_components = [component for component in component_server if
131 (search_query.lower() in component['name'].lower() if component['name'] else False) or
132 (search_query.lower() in component['description'].lower() if component[
133 'description'] else False)]
134 else:
135 filtered_components = component_server
136 current_language = app.config['CURRENT_LANGUAGE'] #get_locale()
137
138 return render_template('dashboard.html', components=components,
139 components_names=components_names, component_server=filtered_components,
140 search_query=search_query, language=current_language)
141
142 @components_bp.route('/show_components/<string:component_id>')
143 @first_login_required
144 def show_components(component_id):
145 response = requests.get('http://127.0.0.1:8001/api/components')
146 json_data = response.json()
147 selected_component = next((component for component in json_data if component['id'] == component_id), None)
148 return render_template('component-info.html', selected_component=selected_component)
149
150 @components_bp.route('/install_components_git/<string:component_id>')
151 @first_login_required
152 def install_components_git(component_id):
153 # Шлях до головної папки проекту
154 project_folder = 'components'
155 #component = Component.get_repository_by_id(component_id)
156 response = requests.get('http://127.0.0.1:8001/api/components')
157 json_data = response.json()
158 selected_component = next((component for component in json_data if component['id'] == component_id), None)
159 # Назва репозиторія
160 repository_name = selected_component['name']
161 # Шлях до папки репозиторія в межах проекту
162 repository_folder = os.path.join(project_folder, repository_name)
163 # URL репозиторія
164
165 git_repo_url = selected_component['git_link']
166 try:
167 # Перевірка наявності папки репозиторія
168 if not os.path.exists(repository_folder):
169 # Створення папки репозиторія
170 os.makedirs(repository_folder)
171 # Шлях до файлу __init__.py
172 init_file = os.path.join(repository_folder, '__init__.py')
173 # Перевірка наявності файлу __init__.py
174 if not os.path.exists(init_file):
175 # Створення пустого файлу __init__.py
176 open(init_file, 'a').close()
177 #підключення до приватного репозиторію
178 #os.environ['GITLAB_PRIVATE_TOKEN'] = '8xxTpxrKVDGoXD5ynjiW'
179 #git_repo_url_with_token = git_repo_url.replace('https://',
180 # f'https://oauth2:{os.environ["GITLAB_PRIVATE_TOKEN"]}@')
181
182 # Команда для встановлення з використанням git_repo_url і повного шляху до папки репозиторія
183 command = ['venv/Scripts/python.exe', '-m', 'pip', 'install', '--use-pep517', 'git+' + git_repo_url, '--target=' + repository_folder]
184 # Виконуємо команду встановлення
185 subprocess.check_call(command)
186
187 # Шлях до файлу, до якого потрібно додати код
188 file_path = 'routes.py'
189 # Код, який потрібно додати
190 code = selected_component['dependencies']
191 # Відкриття файлу у режимі дозапису
192 with open(file_path, 'a') as file:
193 # Запис нового коду у файл
194 file.write('\n')
195 file.write(code)
196 file.write('\n')
197 component = Component.query.filter_by(name=selected_component['name']).first()
198 if not component:
199 new_component = Component(
200 name=selected_component['name'],
201 description=selected_component['description'],
202 version=selected_component['version'],
203 git_link=selected_component['git_link'],
204 dependencies=selected_component['dependencies'],
205 installed=True
206 )
207 # Add the new component to the database
208 db.session.add(new_component)
209 db.session.commit()
210
211 # Повертаємо повідомлення про успішне встановлення
212 return f'Installed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
213 except subprocess.CalledProcessError as e:
214 return 'Error installing : ' + str(e)
215
216 @components_bp.route('/install_components/<string:component_id>')
217 @first_login_required
218 def install_component_from_archive(component_id):
219 # Шлях до головної папки проекту
220 project_folder = 'components'
221 # Отримати відповідну компоненту зі списку компонент
222 response = requests.get('http://127.0.0.1:8001/api/components')
223 json_data = response.json()
224 selected_component = next((component for component in json_data if component['id'] == component_id), None)
225 if selected_component is None:
226 return 'Component not found'
227 # Отримати посилання на архів компоненти та версію
228 archive_url = selected_component['latest_component_data']
229 version = selected_component['latest_version']
230 try:
231 # Створити шлях до папки компоненти згідно назви та версії
232 component_folder = os.path.join(project_folder)
233 os.makedirs(component_folder, exist_ok=True)
234 # Завантажити архів компоненти
235 response = requests.get(archive_url, stream=True)
236 response.raise_for_status()
237 # Шлях до завантаженого архіву
238 archive_path = os.path.join(component_folder, f"{selected_component['name']}.zip")
239 # Встановити залежності з файлу requirements.txt
240
241 # Зберегти архів на диск
242 with open(archive_path, "wb") as file:
243 for chunk in response.iter_content(chunk_size=8192):
244 file.write(chunk)
245 # Розпакувати архів
246 with zipfile.ZipFile(archive_path, "r") as zip_ref:
247 zip_ref.extractall(component_folder)
248
249 # Видалити архів
250 os.remove(archive_path)
251
252 # Шлях до файлу, до якого потрібно додати код
253 file_path = 'routes.py'
254 # Код, який потрібно додати
255 code = f'''from components.{selected_component['name']}.{selected_component['name']}.views import {selected_component['name']}\napp.register_blueprint({selected_component['name']}, url_prefix='/{selected_component['name']}')'''
256 #selected_component['dependencies']
257
258 # Відкриття файлу у режимі дозапису
259 with open(file_path, 'a') as file:
260 # Запис нового коду у файл
261 file.write('\n')
262 file.write(code)
263 file.write('\n')
264
265 # Оновити базу даних з встановленою компонентою
266 component = Component.query.filter_by(name=selected_component['name']).first()
267 if not component:
268 component = Component(
269 name=selected_component['name'],
270 description=selected_component['description'],
271 version=version,
272 git_link=selected_component['git_link'],
273 dependencies=code,
274 installed=True
275 )
276 db.session.add(component)
277 else:
278 component.version = version
279 component.git_link = selected_component['git_link']
280 component.dependencies = code
281 component.installed = True
282
283 db.session.commit()
284
285 return f'''Component installed successfully: {selected_component["name"]} v{version},
286 \n \n please wait installing requirments...
287 <meta http-equiv="refresh" content="0;url=/install-requirments/{selected_component["name"]}" />'''
288
289 except Exception as e:
290 return f'Error installing component: {str(e)}'
291
292 @components_bp.route('/install-requirments/<string:selected_component_name>', methods=['GET'])
293 @first_login_required
294 def install_requirments(selected_component_name):
295 requirements_file = os.path.join('components', selected_component_name, "requirements.txt")
296 if os.path.isfile(requirements_file):
297 pip_command = f"{current_app.config['VENV_BIN_PATH']}/python -m pip install -r {requirements_file}"
298 subprocess.run(pip_command, shell=True, check=True)
299 return f'''Requirements installed successfully
300 \n \n please wait installing requirments components...
301 <meta http-equiv="refresh" content="1;url=/install-requirments-components" />'''
302
303
304 @components_bp.route('/install-requirments-components')
305 @first_login_required
306 def install_requirements_components():
307 # Откриття файлу requirements_components.txt
308 requirements_file = 'requirements_components.txt'
309 component_ids = None
310 if os.path.isfile(requirements_file):
311 with open('requirements_components.txt', 'r') as file:
312 component_ids = file.read().splitlines()
313 if component_ids:
314 for component_id in component_ids:
315 # Виклик роуту '/install_components/<string:component_name>' для кожної назви компоненти
316 response = requests.get(
317 f'/install_components/{component_id}')
318 return f'Requirements components installed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
319 else:
320 return f'<meta http-equiv="refresh" content="1;url=/dashboard" />'
321 # Опрацювання відповіді (за потреби)
322
323
324
325
326 @components_bp.route('/remove_dependencies/<string:component_id>', methods=['GET'])
327 @first_login_required
328 def remove_dependencies(component_id):
329 # Знаходимо компоненту за її ID
330 component = Component.query.get(component_id)
331 if not component:
332 return 'Component not found <meta http-equiv="refresh" content="1;url=/dashboard" />'
333
334 # Шлях до файлу, з якого потрібно видалити залежності
335 file_path = 'routes.py'
336
337 # Зчитуємо вміст файлу
338 with open(file_path, 'r') as file:
339 lines = file.readlines()
340
341 # Видаляємо рядки, що містять залежності компоненти
342 updated_lines = [line for line in lines if line.strip() not in component.dependencies]
343
344 # Записуємо оновлений вміст у файл
345 with open(file_path, 'w') as file:
346 file.writelines(updated_lines)
347
348 # Оновлюємо статус компоненти
349 component.installed = False
350 db.session.commit()
351
352 return 'Component successfully turn off <meta http-equiv="refresh" content="1;url=/dashboard" />'
353
354 @components_bp.route('/add_dependencies/<string:component_id>', methods=['GET'])
355 @first_login_required
356 def add_dependencies(component_id):
357
358 # Знаходимо компоненту за її ID
359 component = Component.query.get(component_id)
360 if not component:
361 return 'Component not found <meta http-equiv="refresh" content="1;url=/dashboard" />'
362
363 # Шлях до файлу, з якого потрібно видалити залежності
364 file_path = 'routes.py'
365 code = component.dependencies
366 # Відкриття файлу у режимі дозапису
367 with open(file_path, 'a') as file:
368 # Запис нового коду у файл
369 file.write(code)
370
371 # Оновлюємо статус компоненти
372 component.installed = True
373 db.session.commit()
374 return 'Dependencies added successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
375
376 @components_bp.route('/remove-component/<string:component_id>')
377 @first_login_required
378 def remove_component(component_id):
379 # Шлях до головної папки проекту
380 project_folder = 'components'
381 component = Component.query.get(component_id)
382 # Назва репозиторія
383 repository_name = component.name
384 # Шлях до папки репозиторія в межах проекту
385 repository_folder = os.path.join(project_folder, repository_name)
386 try:
387 # Перевірка наявності папки репозиторія
388 if os.path.exists(repository_folder):
389 # Видалення папки репозиторія
390 shutil.rmtree(repository_folder)
391 # Видаляємо компоненту з бази
392 if component:
393 db.session.delete(component)
394 db.session.commit()
395
396 # Повертаємо повідомлення про успішне видалення
397 return 'Component removed successfully <meta http-equiv="refresh" content="1;url=/dashboard" />'
398 except Exception as e:
399 # Повертаємо повідомлення про помилку видалення
400 return 'Error removing component: ' + str(e)
401 app.register_blueprint(components_bp)
languages/__pycache__/find_language.cpython-310.pyc
No preview for this file type
languages/__pycache__/views.cpython-310.pyc
No preview for this file type
languages/babel_translation_directories.yml
File was created 1 babel_translation_directories: languages;components/k2test/k2test/languages
2
languages/find_language.py
1 from k2.root import Component File was deleted
2 1 from k2.root import Component
3 2
languages/views.py
1 import yaml File was deleted
2 from flask import session, g
3 from main import app, babel
4
5 def load_languages():
6 with open('languages/languages.yml', 'r') as file:
7 languages = yaml.safe_load(file)
8 return print(languages)
9
10 def get_locale():
11 if 'lang' in session:
12 # Отримати обрану мову з сеансу користувача
13 return session['lang']
14 else:
15 # Повернути мову за замовчуванням з налаштувань додатку
16 return app.config['DEFAULT_LANGUAGE']
17 babel.init_app(app, locale_selector=get_locale)
18 1 import yaml
1 import pytz
2 from flask import Flask
3 from flask_cors import CORS 1 from flask_cors import CORS
4 from db.database import init_db
5 import os
6 import sys
7 from k2.root import *
8 from flask_login import LoginManager
9 from flask_babel import Babel 2 from flask_babel import Babel
3 #from k2.k2inst import *
4 from flask import Flask
5 from k2.k2cfg import k2
10 6
11
12 # configure flask app 7 # configure flask app
13 app = Flask(__name__) 8 app = Flask(__name__)
14 venv_path = os.path.join(os.path.dirname(sys.prefix), 'venv/Scripts') 9 app.config['SECRET_KEY'] = k2.secret_key
15 app.config['VENV_BIN_PATH'] = venv_path 10 app.config.from_object(k2)
16 app.config['JSON_SORT_KEYS'] = False
17 app.config['TIMEZONE'] = pytz.timezone('Europe/Kiev')
18 app.config['SECRET_KEY'] = 'JH&INH987gFDHdsagh&8dwbjdw8ckw'
19 app.config['DOMAIN'] = 'http://127.0.0.1:5001/'
20 app.config['DEFAULT_LANGUAGE'] = 'en'
21 app.config['CURRENT_LANGUAGE'] = ''
22 app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'languages;components/k2test/k2test/languages'
23 11
24 # setting flask app 12 # init db
25 init_db(app) 13 k2().init_db(app)
26 CORS(app) 14 CORS(app)
15
16 #translate
17 k2.load_babel_translation_directories()
18 app.config['BABEL_TRANSLATION_DIRECTORIES'] = k2.babel_translation_directories
27 babel = Babel(app) 19 babel = Babel(app)
20 babel.init_app(app, locale_selector=k2.get_locale)
1 from flask import Flask 1 from flask import Flask
2 #app = Flask(__name__) 2 #app = Flask(__name__)
3 from main import app, babel 3 from main import app, babel
4 from k2.k2inst import components_bp
5 app.register_blueprint(components_bp)
6
7 from components.k2test.k2test.views import k2test
8 app.register_blueprint(k2test, url_prefix='/k2test')
4 9
templates/dashboard.html
1 {% extends "base.html" %} 1 {% extends "base.html" %}
2 2
3 {% block styles %} 3 {% block styles %}
4 <style> 4 <style>
5 .table { 5 .table {
6 border-collapse: collapse; 6 border-collapse: collapse;
7 width: 100%; 7 width: 100%;
8 } 8 }
9 9
10 .table th, .table td { 10 .table th, .table td {
11 text-align: left; 11 text-align: left;
12 padding: 8px; 12 padding: 8px;
13 border-bottom: 1px solid #ddd; 13 border-bottom: 1px solid #ddd;
14 } 14 }
15 15
16 .table tr:hover { 16 .table tr:hover {
17 background-color: #f5f5f5; 17 background-color: #f5f5f5;
18 } 18 }
19 19
20 .search-form { 20 .search-form {
21 margin-bottom: 20px; 21 margin-bottom: 20px;
22 } 22 }
23 23
24 .search-form .input-group { 24 .search-form .input-group {
25 width: 300px; 25 width: 300px;
26 } 26 }
27 </style> 27 </style>
28 {% endblock %} 28 {% endblock %}
29 29
30 {% block content %} 30 {% block content %}
31 <h1>K2CloudERP-2.5.1.27</h1> 31 <h1>K2CloudERP-2.5.1.27</h1>
32 <p> <a href="/logout">вихід</a></p> 32 <p> <a href="/logout">вихід</a></p>
33 <p> <a href="{{ url_for('components.change_language', lang='en') }}">en</a> | <a href="{{ url_for('components.change_language', lang='uk') }}">uk</a></p> 33 <p> <a href="{{ url_for('components.change_language', lang='en') }}">en</a> | <a href="{{ url_for('components.change_language', lang='uk') }}">uk</a></p>
34 {{language}} 34 {{language}}
35 {{lang_d}}
35 36
36 <h2>{{ _('Installed components') }}</h2> 37 <h2>{{ _('Installed components') }}</h2>
37 38
38 <table class="table"> 39 <table class="table">
39 <thead> 40 <thead>
40 <tr> 41 <tr>
41 <th>{{_('Name')}}</th> 42 <th>{{_('Name')}}</th>
42 <th>{{_('Description')}}</th> 43 <th>{{_('Description')}}</th>
43 <th>{{_('Version')}}</th> 44 <th>{{_('Version')}}</th>
44 <th>{{_('Dependencies')}}</th> 45 <th>{{_('Dependencies')}}</th>
45 <th>{{_('Git Link')}}</th> 46 <th>{{_('Git Link')}}</th>
46 <th>{{_('Install At')}}</th> 47 <th>{{_('Install At')}}</th>
47 <th>{{_('Status')}}</th> 48 <th>{{_('Status')}}</th>
48 <th>{{_('Action')}}</th> 49 <th>{{_('Action')}}</th>
49 </tr> 50 </tr>
50 </thead> 51 </thead>
51 52
52 <tbody> 53 <tbody>
53 <tr> 54 <tr>
54 <td>k2root</td> 55 <td>k2root</td>
55 <td>main component</td> 56 <td>main component</td>
56 <td colspan="6">2.5.1.27</td></tr> 57 <td colspan="6">2.5.1.27</td></tr>
57 58
58 {% for component in components %} 59 {% for component in components %}
59 <tr> 60 <tr>
60 <td>{{ component.name }}</td> 61 <td>{{ component.name }}</td>
61 <td>{{ component.description }}</td> 62 <td>{{ component.description }}</td>
62 <td>{{ component.version }}</td> 63 <td>{{ component.version }}</td>
63 <td>{{ component.dependencies }}</td> 64 <td>{{ component.dependencies }}</td>
64 <td>{{ component.git_link }}</td> 65 <td>{{ component.git_link }}</td>
65 <td>{{ component.date_installed }}</td> 66 <td>{{ component.date_installed }}</td>
66 <td> 67 <td>
67 {% if component.installed %} 68 {% if component.installed %}
68 Yes 69 Yes
69 {% else %} 70 {% else %}
70 No 71 No
71 {% endif %} 72 {% endif %}
72 </td> 73 </td>
73 <td> 74 <td>
74 {% if component.installed %} 75 {% if component.installed %}
75 <a href="/remove_dependencies/{{ component.id }}" class="btn btn-primary">Вимкнути</a> 76 <a href="/remove_dependencies/{{ component.id }}" class="btn btn-primary">Вимкнути</a>
76 {% else %} 77 {% else %}
77 <a href="/add_dependencies/{{ component.id }}" class="btn btn-primary">Увімкнути</a> 78 <a href="/add_dependencies/{{ component.id }}" class="btn btn-primary">Увімкнути</a>
78 <!-- Button trigger modal --> 79 <!-- Button trigger modal -->
79 <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#components-{{ component.id }}"> 80 <button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#components-{{ component.id }}">
80 Видалити 81 Видалити
81 </button> 82 </button>
82 83
83 <!-- Modal --> 84 <!-- Modal -->
84 <div class="modal fade" id="components-{{ component.id }}" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true"> 85 <div class="modal fade" id="components-{{ component.id }}" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
85 <div class="modal-dialog"> 86 <div class="modal-dialog">
86 <div class="modal-content"> 87 <div class="modal-content">
87 <div class="modal-header"> 88 <div class="modal-header">
88 <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1> 89 <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
89 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 90 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
90 </div> 91 </div>
91 <div class="modal-body"> 92 <div class="modal-body">
92 Видалити компоненту? 93 Видалити компоненту?
93 </div> 94 </div>
94 <div class="modal-footer"> 95 <div class="modal-footer">
95 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> 96 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
96 <a href="/remove-component/{{ component.id }}" class="btn btn-danger">Видалити</a> 97 <a href="/remove-component/{{ component.id }}" class="btn btn-danger">Видалити</a>
97 </div> 98 </div>
98 </div> 99 </div>
99 </div> 100 </div>
100 </div> 101 </div>
101 {% endif %} 102 {% endif %}
102 </td> 103 </td>
103 </tr> 104 </tr>
104 {% endfor %} 105 {% endfor %}
105 </tbody> 106 </tbody>
106 </table> 107 </table>
107 108
108 109
109 110
110 111
111 112
112 113
113 <div class="container px-4 py-5" id="hanging-icons"> 114 <div class="container px-4 py-5" id="hanging-icons">
114 <h2 class="pb-2 border-bottom">{{_('Components Shop')}}</h2> 115 <h2 class="pb-2 border-bottom">{{_('Components Shop')}}</h2>
115 <form action="{{ url_for('components.dashboard') }}" method="GET" class="search-form"> 116 <form action="{{ url_for('components.dashboard') }}" method="GET" class="search-form">
116 <div class="input-group"> 117 <div class="input-group">
117 <input type="text" name="search_query" class="form-control" placeholder="Search components"> 118 <input type="text" name="search_query" class="form-control" placeholder="Search components">
118 <button type="submit" class="btn btn-primary">Search</button> 119 <button type="submit" class="btn btn-primary">Search</button>
119 </div> 120 </div>
120 </form> 121 </form>
121 122
122 123
123 <div class="row g-4 py-5 row-cols-1 row-cols-lg-3"> 124 <div class="row g-4 py-5 row-cols-1 row-cols-lg-3">
124 {% if component_server %} 125 {% if component_server %}
125 {% for component in component_server %} 126 {% for component in component_server %}
126 <div class="col d-flex align-items-start"> 127 <div class="col d-flex align-items-start">
127 <div class="icon-square bg-light text-dark flex-shrink-0 me-3"> 128 <div class="icon-square bg-light text-dark flex-shrink-0 me-3">
128 <img class="d-block mx-auto mb-4" src="/static/img/logo-k2.png" alt="" width="60"> 129 <img class="d-block mx-auto mb-4" src="/static/img/logo-k2.png" alt="" width="60">
129 </div> 130 </div>
130 <div> 131 <div>
131 <h2>{{ component.name|title }}</h2> 132 <h2>{{ component.name|title }}</h2>
132 <p>{{ component.description }}</p> 133 <p>{{ component.description }}</p>
133 <p>ВЕРСІЯ {{ component.version }}</p> 134 <p>ВЕРСІЯ {{ component.latest_version }}</p>
134 {% if component.name in components_names %} 135 {% if component.name in components_names %}
135 <button type="button" class="btn btn-success">Installed</button> 136 <button type="button" class="btn btn-success">Installed</button>
136 {% else %} 137 {% else %}
137 <a href="/show_components/{{ component.id }}" class="btn btn-primary">Install</a> 138 <a href="/show_components/{{ component.id }}" class="btn btn-primary">Install</a>
138 {% endif %} 139 {% endif %}
139 </div> 140 </div>
140 </div> 141 </div>
141 {% endfor %} 142 {% endfor %}
142 {% else %} 143 {% else %}
143 <div class="col"> 144 <div class="col">
144 <p>No components available.</p> 145 <p>No components available.</p>
145 </div> 146 </div>
146 {% endif %} 147 {% endif %}
147 </div> 148 </div>
148 </div> 149 </div>
149 150
150 {% endblock %} 151 {% endblock %}
151 152