Added html links for equipment/processes/tips.
This commit is contained in:
@@ -55,9 +55,10 @@ def update_log(mapper, connection, target):
|
||||
continue
|
||||
added = [str(item) for item in hist.added]
|
||||
# NOTE: Attributes left out to save space
|
||||
if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations',
|
||||
'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
|
||||
'gel_controls', 'source_plates']:
|
||||
# if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations',
|
||||
# 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
|
||||
# 'gel_controls', 'source_plates']:
|
||||
if attr.key in LogMixin.tracking_exclusion:
|
||||
continue
|
||||
deleted = [str(item) for item in hist.deleted]
|
||||
change = dict(field=attr.key, added=added, deleted=deleted)
|
||||
|
||||
@@ -9,7 +9,7 @@ from sqlalchemy import Column, INTEGER, String, JSON
|
||||
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
from typing import Any, List
|
||||
from typing import Any, List, ClassVar
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm.relationships import _RelationshipDeclared
|
||||
from tools import report_result, list_sort_dict
|
||||
@@ -25,6 +25,12 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class LogMixin(Base):
|
||||
|
||||
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
|
||||
'submission_reagent_associations', 'submission_equipment_associations',
|
||||
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
|
||||
'source_plates']
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
@property
|
||||
|
||||
@@ -4,12 +4,15 @@ All kit and reagent related models
|
||||
from __future__ import annotations
|
||||
import json, zipfile, yaml, logging, re, sys
|
||||
from pprint import pformat
|
||||
|
||||
from jinja2 import Template, TemplateNotFound
|
||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
|
||||
from sqlalchemy.orm import relationship, validates, Query
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from datetime import date, datetime, timedelta
|
||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
|
||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
|
||||
jinja_template_loading
|
||||
from typing import List, Literal, Generator, Any, Tuple
|
||||
from pandas import ExcelFile
|
||||
from pathlib import Path
|
||||
@@ -2065,6 +2068,53 @@ class Equipment(BaseClass, LogMixin):
|
||||
output.append(equipment[choice])
|
||||
return output
|
||||
|
||||
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
|
||||
"""
|
||||
dictionary containing values necessary for gui
|
||||
|
||||
Args:
|
||||
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: representation of the equipment's attributes
|
||||
"""
|
||||
if self.nickname:
|
||||
nickname = self.nickname
|
||||
else:
|
||||
nickname = self.name
|
||||
output = dict(
|
||||
name=self.name,
|
||||
nickname=nickname,
|
||||
asset_number=self.asset_number
|
||||
)
|
||||
if full_data:
|
||||
subs = []
|
||||
output['submissions'] = [dict(plate=item.submission.rsl_plate_num, process=item.process.name)
|
||||
if item.process else dict(plate=item.submission.rsl_plate_num, process="NA")
|
||||
for item in self.equipment_submission_associations]
|
||||
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
|
||||
return output
|
||||
|
||||
@classproperty
|
||||
def details_template(cls) -> Template:
|
||||
"""
|
||||
Get the details jinja template for the correct class
|
||||
|
||||
Args:
|
||||
base_dict (dict): incoming dictionary of Submission fields
|
||||
|
||||
Returns:
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
try:
|
||||
template = env.get_template(temp_name)
|
||||
except TemplateNotFound as e:
|
||||
logger.error(f"Couldn't find template {e}")
|
||||
template = env.get_template("equipment_details.html")
|
||||
return template
|
||||
|
||||
|
||||
class EquipmentRole(BaseClass):
|
||||
"""
|
||||
@@ -2219,6 +2269,10 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
self.equipment = equipment
|
||||
self.role = role
|
||||
|
||||
@property
|
||||
def process(self):
|
||||
return Process.query(id=self.process_id)
|
||||
|
||||
def to_sub_dict(self) -> dict:
|
||||
"""
|
||||
This SubmissionEquipmentAssociation as a dictionary
|
||||
@@ -2433,6 +2487,44 @@ class Process(BaseClass):
|
||||
tip_roles=tip_roles
|
||||
)
|
||||
|
||||
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
|
||||
"""
|
||||
dictionary containing values necessary for gui
|
||||
|
||||
Args:
|
||||
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: representation of the equipment's attributes
|
||||
"""
|
||||
output = dict(
|
||||
name=self.name,
|
||||
)
|
||||
if full_data:
|
||||
output['submissions'] = [dict(plate=sub.submission.rsl_plate_num, equipment=sub.equipment.name) for sub in self.submissions]
|
||||
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
|
||||
return output
|
||||
|
||||
@classproperty
|
||||
def details_template(cls) -> Template:
|
||||
"""
|
||||
Get the details jinja template for the correct class
|
||||
|
||||
Args:
|
||||
base_dict (dict): incoming dictionary of Submission fields
|
||||
|
||||
Returns:
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
try:
|
||||
template = env.get_template(temp_name)
|
||||
except TemplateNotFound as e:
|
||||
logger.error(f"Couldn't find template {e}")
|
||||
template = env.get_template("process_details.html")
|
||||
return template
|
||||
|
||||
|
||||
class TipRole(BaseClass):
|
||||
"""
|
||||
@@ -2576,6 +2668,45 @@ class Tips(BaseClass, LogMixin):
|
||||
name=self.name
|
||||
)
|
||||
|
||||
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
|
||||
"""
|
||||
dictionary containing values necessary for gui
|
||||
|
||||
Args:
|
||||
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: representation of the equipment's attributes
|
||||
"""
|
||||
output = dict(
|
||||
name=self.name,
|
||||
lot=self.lot,
|
||||
)
|
||||
if full_data:
|
||||
output['submissions'] = [dict(plate=item.submission.rsl_plate_num, role=item.role_name)
|
||||
for item in self.tips_submission_associations]
|
||||
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
|
||||
return output
|
||||
|
||||
@classproperty
|
||||
def details_template(cls) -> Template:
|
||||
"""
|
||||
Get the details jinja template for the correct class
|
||||
|
||||
Args:
|
||||
base_dict (dict): incoming dictionary of Submission fields
|
||||
|
||||
Returns:
|
||||
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
|
||||
"""
|
||||
env = jinja_template_loading()
|
||||
temp_name = f"{cls.__name__.lower()}_details.html"
|
||||
try:
|
||||
template = env.get_template(temp_name)
|
||||
except TemplateNotFound as e:
|
||||
logger.error(f"Couldn't find template {e}")
|
||||
template = env.get_template("tips_details.html")
|
||||
return template
|
||||
|
||||
class SubmissionTypeTipRoleAssociation(BaseClass):
|
||||
"""
|
||||
|
||||
@@ -133,8 +133,8 @@ class CustomFigure(Figure):
|
||||
else:
|
||||
html += "<h1>No data was retrieved for the given parameters.</h1>"
|
||||
html += '</body></html>'
|
||||
with open("test.html", "w", encoding="utf-8") as f:
|
||||
f.write(html)
|
||||
# with open("test.html", "w", encoding="utf-8") as f:
|
||||
# f.write(html)
|
||||
return html
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtWebChannel import QWebChannel
|
||||
from PyQt6.QtCore import Qt, pyqtSlot
|
||||
from jinja2 import TemplateNotFound
|
||||
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
|
||||
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType, Equipment, Process, Tips
|
||||
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
|
||||
from .functions import select_save_file, save_pdf
|
||||
from pathlib import Path
|
||||
@@ -84,6 +84,48 @@ class SubmissionDetails(QDialog):
|
||||
else:
|
||||
self.back.setEnabled(True)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def equipment_details(self, equipment: str | Equipment):
|
||||
logger.debug(f"Equipment details")
|
||||
if isinstance(equipment, str):
|
||||
equipment = Equipment.query(name=equipment)
|
||||
base_dict = equipment.to_sub_dict(full_data=True)
|
||||
template = equipment.details_template
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(equipment=base_dict, css=css)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Equipment Details - {equipment.name}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def process_details(self, process: str | Process):
|
||||
logger.debug(f"Equipment details")
|
||||
if isinstance(process, str):
|
||||
process = Process.query(name=process)
|
||||
base_dict = process.to_sub_dict(full_data=True)
|
||||
template = process.details_template
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(process=base_dict, css=css)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Process Details - {process.name}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def tips_details(self, tips: str | Tips):
|
||||
logger.debug(f"Equipment details: {tips}")
|
||||
if isinstance(tips, str):
|
||||
tips = Tips.query(lot=tips)
|
||||
base_dict = tips.to_sub_dict(full_data=True)
|
||||
template = tips.details_template
|
||||
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(tips=base_dict, css=css)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Process Details - {tips.name}")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sample_details(self, sample: str | BasicSample):
|
||||
"""
|
||||
@@ -103,8 +145,8 @@ class SubmissionDetails(QDialog):
|
||||
with open(template_path.joinpath("css", "styles.css"), "r") as f:
|
||||
css = f.read()
|
||||
html = template.render(sample=base_dict, css=css)
|
||||
with open(f"{sample.submitter_id}.html", 'w') as f:
|
||||
f.write(html)
|
||||
# with open(f"{sample.submitter_id}.html", 'w') as f:
|
||||
# f.write(html)
|
||||
self.webview.setHtml(html)
|
||||
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")
|
||||
|
||||
|
||||
@@ -20,19 +20,19 @@
|
||||
{% if sub['reagents'] %}
|
||||
<h3><u>Reagents:</u></h3>
|
||||
<p>{% for item in sub['reagents'] %}
|
||||
<b>{{ item['role'] }}</b>: <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
|
||||
<b>{{ item['role'] }}:</b> <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['equipment'] %}
|
||||
<h3><u>Equipment:</u></h3>
|
||||
<p>{% for item in sub['equipment'] %}
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br> ') }}<br>
|
||||
<b>{{ item['role'] }}:</b> <a class="data-link equipment" id="{{ item['name'] }}"> {{ item['name'] }} ({{ item['asset_number'] }})</a>: <a class="data-link process" id="{{ item['processes'][0]|replace('\n\t', '') }}">{{ item['processes'][0]|replace('\n\t', '<br> ') }}</a><br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['tips'] %}
|
||||
<h3><u>Tips:</u></h3>
|
||||
<p>{% for item in sub['tips'] %}
|
||||
<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['lot'] }})<br>
|
||||
<b>{{ item['role'] }}:</b> <a class="data-link tips" id="{{ item['lot'] }}">{{ item['name'] }} ({{ item['lot'] }})</a><br>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if sub['samples'] %}
|
||||
@@ -99,6 +99,33 @@
|
||||
})
|
||||
}
|
||||
|
||||
var equipmentSelection = document.getElementsByClassName('equipment');
|
||||
|
||||
for(let i = 0; i < equipmentSelection.length; i++) {
|
||||
equipmentSelection[i].addEventListener("click", function() {
|
||||
console.log(equipmentSelection[i].id);
|
||||
backend.equipment_details(equipmentSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
var processSelection = document.getElementsByClassName('process');
|
||||
|
||||
for(let i = 0; i < processSelection.length; i++) {
|
||||
processSelection[i].addEventListener("click", function() {
|
||||
console.log(processSelection[i].id);
|
||||
backend.process_details(processSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
var tipsSelection = document.getElementsByClassName('tips');
|
||||
|
||||
for(let i = 0; i < tipsSelection.length; i++) {
|
||||
tipsSelection[i].addEventListener("click", function() {
|
||||
console.log(tipsSelection[i].id);
|
||||
backend.tips_details(tipsSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
document.getElementById("sign_btn").addEventListener("click", function(){
|
||||
backend.sign_off("{{ sub['plate_number'] }}");
|
||||
});
|
||||
|
||||
50
src/submissions/templates/equipment_details.html
Normal file
50
src/submissions/templates/equipment_details.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "details.html" %}
|
||||
<head>
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Equipment Details for {{ equipment['name'] }}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<h2><u>Equipment Details for {{ equipment['name'] }}</u></h2>
|
||||
{{ super() }}
|
||||
<p>{% for key, value in equipment.items() if key not in equipment['excluded'] %}
|
||||
<!-- <b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
|
||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||
{% endfor %}</p>
|
||||
<!-- {% if permission %}-->
|
||||
<!-- <button type="button" id="save_btn">Save</button>-->
|
||||
<!-- {% endif %}-->
|
||||
{% if equipment['submissions'] %}<h2>Submissions:</h2>
|
||||
{% for submission in equipment['submissions'] %}
|
||||
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b> <a class="data-link process" id="{{ submission['process'] }}">{{ submission['process'] }}</a></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
var processSelection = document.getElementsByClassName('process');
|
||||
|
||||
for(let i = 0; i < processSelection.length; i++) {
|
||||
processSelection[i].addEventListener("click", function() {
|
||||
console.log(processSelection[i].id);
|
||||
backend.process_details(processSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
{% for submission in equipment['submissions'] %}
|
||||
document.getElementById("{{ submission }}").addEventListener("click", function(){
|
||||
backend.submission_details("{{ submission }}");
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
{% endblock %}
|
||||
</script>
|
||||
</html>
|
||||
49
src/submissions/templates/process_details.html
Normal file
49
src/submissions/templates/process_details.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "details.html" %}
|
||||
<head>
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Process Details for {{ process['name'] }}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<h2><u>Process Details for {{ process['name'] }}</u></h2>
|
||||
{{ super() }}
|
||||
<p>{% for key, value in process.items() if key not in process['excluded'] %}
|
||||
<!-- <b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
|
||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||
{% endfor %}</p>
|
||||
<!-- {% if permission %}-->
|
||||
<!-- <button type="button" id="save_btn">Save</button>-->
|
||||
<!-- {% endif %}-->
|
||||
{% if process['submissions'] %}<h2>Submissions:</h2>
|
||||
{% for submission in process['submissions'] %}
|
||||
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b><a class="data-link equipment" id="{{ submission['equipment'] }}">{{ submission['equipment'] }}</a></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
var equipmentSelection = document.getElementsByClassName('equipment');
|
||||
|
||||
for(let i = 0; i < equipmentSelection.length; i++) {
|
||||
equipmentSelection[i].addEventListener("click", function() {
|
||||
console.log(equipmentSelection[i].id);
|
||||
backend.equipment_details(equipmentSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
{% for submission in process['submissions'] %}
|
||||
document.getElementById("{{ submission['plate'] }}").addEventListener("click", function(){
|
||||
backend.submission_details("{{ submission['plate'] }}");
|
||||
});
|
||||
{% endfor %}
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
{% endblock %}
|
||||
</script>
|
||||
</html>
|
||||
50
src/submissions/templates/tips_details.html
Normal file
50
src/submissions/templates/tips_details.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{% extends "details.html" %}
|
||||
<head>
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<title>Tips Details for {{ tips['name'] }}</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
<h2><u>Tips Details for {{ tips['name'] }}</u></h2>
|
||||
{{ super() }}
|
||||
<p>{% for key, value in tips.items() if key not in tips['excluded'] %}
|
||||
<!-- <b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
|
||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||
{% endfor %}</p>
|
||||
<!-- {% if permission %}-->
|
||||
<!-- <button type="button" id="save_btn">Save</button>-->
|
||||
<!-- {% endif %}-->
|
||||
{% if tips['submissions'] %}<h2>Submissions:</h2>
|
||||
{% for submission in tips['submissions'] %}
|
||||
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b> <a class="data-link process" id="{{ submission['role'] }}">{{ submission['role'] }}</a></p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
<script>
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
|
||||
var processSelection = document.getElementsByClassName('process');
|
||||
|
||||
for(let i = 0; i < processSelection.length; i++) {
|
||||
processSelection[i].addEventListener("click", function() {
|
||||
console.log(processSelection[i].id);
|
||||
backend.process_details(processSelection[i].id);
|
||||
})
|
||||
}
|
||||
|
||||
{% for submission in tips['submissions'] %}
|
||||
document.getElementById("{{ submission }}").addEventListener("click", function(){
|
||||
backend.submission_details("{{ submission }}");
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
backend.activate_export(false);
|
||||
}, false);
|
||||
{% endblock %}
|
||||
</script>
|
||||
</html>
|
||||
Reference in New Issue
Block a user