update documentation, fix some cases
This commit is contained in:
parent
6681f7dfd5
commit
8c68d50964
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
venv
|
||||
src/tww/data/.cache.csv
|
||||
tww/data/.cache.csv
|
||||
__pycache__
|
||||
.idea
|
||||
tww.egg-info
|
41
README.md
41
README.md
@ -3,15 +3,40 @@ Find time now, in the past or future in any timezone or location.
|
||||
|
||||
## Usage
|
||||
```
|
||||
python tz.py QUERY [--format="%Y-%m-%d %H:%M:%S%z"] [--debug]
|
||||
python tww QUERY [--debug] [--full]
|
||||
```
|
||||
|
||||
* `QUERY` - is of the form `<datetime-like> to <timezone or location>`
|
||||
* Supported `QUERY` types:
|
||||
- Timezone translation `<datetime-like> in <timezone/location> to <destination timezone/location>` or `<datetime-like> to <timezone/location>` (assumes datetime is `local`)
|
||||
- `04:26 in japan to local`
|
||||
- `03:14 in local to IST`
|
||||
- `15:20 to America/New_York`
|
||||
- `2021-12-25 12:00 in Brazil`
|
||||
- Time difference: `(time) between <datetime-like> and <datetime-like>` or `(time) since <datetime-like` or `(time) until <datetime-like>`
|
||||
- `time between 2012-03-14 and 2012-04-26`
|
||||
- `time since 09:00`
|
||||
- `time until end of workday`
|
||||
- (Approximate) workdays calculation (assumes monday-friday are work days - ignores public/local holidays (for now)): `work days/hours since/until <datetime-like>` or `
|
||||
- `workdays since 2021-01-05`
|
||||
- `work hours until Friday`
|
||||
- Milliseconds since epoch: `(time/seconds) since epoch`
|
||||
- Datetime to epoch: `<datetime-like> to epoch` or `(milli)seconds since <datetime-like>/epoch`
|
||||
- `2021-01:01 to epoch`
|
||||
- `milliseconds since epoch`
|
||||
|
||||
|
||||
Few more notes:
|
||||
- `<datetime-like>` is any time or time-like string - or example:
|
||||
- `2019-04-26 3:14`, `06:42` `27 January 1992`,
|
||||
- some human readable like `now`, `in 3 hours`, `7 minutes ago` and many others.
|
||||
- See [dateparser](https://pypi.org/project/dateparser/) for more.
|
||||
- custom date-times (like `christmas`, `new years`, `end of workday`) defined in `data/custom_dt.csv`
|
||||
|
||||
`<datetime-like>` is any time or time-like string - or example `2019-04-26 3:14`, `06:42`, timezones `15:10 cet` but also some human readable like `now`, `in 3 hours`, `7 minutes ago` and many others. See [dateparser](https://pypi.org/project/dateparser/) for more.
|
||||
|
||||
`to <timezone or location>` is optional. It has to have the word `to` which specifies that timezone or location follows. It is either:
|
||||
- timezone (tried first) to which the date should be translated or
|
||||
- `<timezone/location>` is either:
|
||||
- timezone (tried first) - it can be:
|
||||
- [tz database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (like `Europe/Sofia`),
|
||||
- [UTC time offset](https://en.wikipedia.org/wiki/List_of_UTC_time_offsets) (with colon `:` or not like `+02:00` or `+0530`), or
|
||||
- [abbreviation](https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations) (like `UTC`, `CET`, `PST` - however please note that these are not unique and resolution might wrong)
|
||||
- a location. Uses a local database of files of countries and cities. It then tries to fuzzymatch the query using [fuzzywuzzy](https://github.com/seatgeek/fuzzywuzzy). In case it can't find the country or city, it uses [geopy](https://geopy.readthedocs.io/en/stable/) for location resolution. Finally it uses [timezonefinder](https://pypi.org/project/timezonefinder/) for timezone resolution.
|
||||
|
||||
* `--format` is the format of the time to be displayed. See supported [datetime formats](https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior)
|
||||
@ -27,10 +52,10 @@ pip install -r requirements.txt
|
||||
You could alias the whole command to `tww` for faster typing, e.g. in your `.bashrc`:
|
||||
|
||||
```
|
||||
alias tww="~/workspace/tz/venv/bin/python ~/workspace/tz/tz.py"
|
||||
alias tww="~/workspace/tww/venv/bin/python ~/workspace/tww/main.py"
|
||||
```
|
||||
|
||||
## Examples
|
||||
## More Examples
|
||||
|
||||
Time now (in this timezone):
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
sofia,42.6975135,23.3241463
|
||||
india,20.0,77.0
|
||||
is,64.9841821,-18.1059013
|
||||
u,42.6384261,12.674297
|
||||
i,22.3511148,78.6677428
|
||||
ut,39.4225192,-111.7143584
|
||||
so,8.3676771,49.083416
|
||||
sofi,42.6975135,23.3241463
|
||||
z,-48.5693327,-70.1606767
|
||||
zu,47.1666667,8.5166664
|
||||
zur,47.1666667,8.5166664
|
||||
zuri,47.1666667,8.5166664
|
||||
zuric,47.3666667,8.5500002
|
||||
zurich,47.3666667,8.5500002
|
||||
gl,77.6192349,-42.8125967
|
||||
gla,37.08034,14.2306747
|
||||
glas,37.08034,14.2306747
|
||||
glasg,37.08034,14.2306747
|
||||
glasgow,55.8333333,-4.25
|
||||
L,33.7680065,66.2385139
|
||||
lo,-17.6394444,-71.3375015
|
||||
L,33.7680065,66.2385139
|
||||
lond,31.9513889,34.8952789
|
||||
londo,51.5084153,-0.1255327
|
||||
london,51.5084153,-0.1255327
|
||||
london ,51.5084153,-0.1255327
|
||||
ne,6.9833333,171.6999969
|
||||
n,64.5731537,11.52803643954819
|
||||
new,6.9833333,171.6999969
|
||||
new ,6.9833333,171.6999969
|
||||
new yo,6.9833333,171.6999969
|
||||
new yor,6.9833333,171.6999969
|
||||
new york,6.9833333,171.6999969
|
||||
new york ,6.9833333,171.6999969
|
||||
mo,14.1194444,15.3133335
|
||||
M,-13.2687204,33.9301963
|
||||
mou,14.1194444,15.3133335
|
||||
mount,14.1194444,15.3133335
|
||||
M,-13.2687204,33.9301963
|
||||
mounta,14.1194444,15.3133335
|
||||
mountai,14.1194444,15.3133335
|
||||
mountain ,14.1194444,15.3133335
|
||||
mountain vi,14.1194444,15.3133335
|
||||
mountain view,14.1194444,15.3133335
|
||||
in,6.9833333,171.6999969
|
||||
ind,20.0,77.0
|
||||
indi,20.0,77.0
|
||||
local,37.6666667,-1.7
|
||||
+02:,49.453285449999996,3.606899003594057
|
||||
it,33.6366667,42.8224983
|
||||
G,32.3293809,-83.1137366
|
||||
D,10.0,-67.166667
|
||||
dub,-32.25,148.6166687
|
||||
D,10.0,-67.166667
|
||||
dubli,53.3330556,-6.248889
|
||||
dublin,53.3330556,-6.248889
|
||||
Du,25.0750095,55.18876088183319
|
||||
dublin ,53.3330556,-6.248889
|
||||
S,35.7724185,127.79654346305617
|
||||
sof,42.6975135,23.3241463
|
||||
S,35.7724185,127.79654346305617
|
||||
sofia ,42.6975135,23.3241463
|
||||
S,35.7724185,127.79654346305617
|
||||
S,35.7724185,127.79654346305617
|
||||
du,-32.25,148.6166687
|
||||
D,10.0,-67.166667
|
||||
S,35.7724185,127.79654346305617
|
||||
S,35.7724185,127.79654346305617
|
||||
S,35.7724185,127.79654346305617
|
||||
S,35.7724185,127.79654346305617
|
||||
D,10.0,-67.166667
|
||||
D,10.0,-67.166667
|
||||
new y,6.9833333,171.6999969
|
||||
S,35.7724185,127.79654346305617
|
||||
S,35.7724185,127.79654346305617
|
||||
D,10.0,-67.166667
|
||||
dubl,53.3330556,-6.248889
|
||||
D,10.0,-67.166667
|
|
47
tww/lib.py
47
tww/lib.py
@ -206,7 +206,6 @@ def resolve_location_local(query):
|
||||
return ""
|
||||
ratio, location = result
|
||||
logger.debug("Location result ({}): {}".format(-ratio, location))
|
||||
write_to_cache(query, location)
|
||||
return location
|
||||
|
||||
|
||||
@ -757,20 +756,60 @@ def td_remainders(td):
|
||||
minutes, seconds = divmod(abs(int(seconds)), 60)
|
||||
hours, minutes = divmod(abs(int(minutes)), 60)
|
||||
days, hours = divmod(abs(int(hours)), 24)
|
||||
months, days = divmod(abs(int(days)), 30.42)
|
||||
total_days = days
|
||||
months, days = divmod(abs(int(days)), 30)
|
||||
years, months = divmod(abs(int(months)), 12)
|
||||
years, months, days, hours, minutes, seconds = map(int, (years, months, days, hours, minutes, seconds))
|
||||
years, months, days, hours, minutes, seconds = map(abs, (years, months, days, hours, minutes, seconds))
|
||||
|
||||
total_weeks, _ = divmod(abs(int(total_days)), 7)
|
||||
total_months, _ = divmod(abs(int(total_days)), 30)
|
||||
|
||||
days_years = total_days - 365 * years
|
||||
days_weeks = total_days - 7 * total_weeks
|
||||
days_months = total_days - 30 * total_months
|
||||
weeks_months, _ = divmod(abs(int(total_days - 30 * total_months)), 7)
|
||||
days_weeks_months = total_days - 30 * total_months - 7 * weeks_months
|
||||
|
||||
years, months, days, year_days, hours, minutes, seconds = map(int, (years, months, days, days_years, hours, minutes, seconds))
|
||||
years, months, days, year_days, hours, minutes, seconds = map(abs, (years, months, days, year_days, hours, minutes, seconds))
|
||||
return dict(
|
||||
seconds=seconds,
|
||||
minutes=minutes,
|
||||
hours=hours,
|
||||
days=days,
|
||||
weeks_months=weeks_months, # how many weeks in addition to "months"
|
||||
days_years=days_years, # how many days in addition to "years"
|
||||
days_months=days_months, # how many days in addition to "months"
|
||||
days_weeks=days_weeks, # how many days in addition to "weeks"
|
||||
days_weeks_months=days_weeks_months, # how many days in addition to "months" and "weeks"
|
||||
months=months,
|
||||
years=years,
|
||||
)
|
||||
|
||||
|
||||
def td_machine(remainders):
|
||||
years, days_years, hours, minutes, seconds = map(lambda x: remainders[x],
|
||||
["years", "days_years", "hours", "minutes", "seconds"])
|
||||
return "{} years, {} days, {:02d}:{:02d}:{:02d}".format(years, days_years, hours, minutes, seconds)
|
||||
|
||||
|
||||
def td_human(remainders):
|
||||
rv = ""
|
||||
for x in ["years", "months", "days", "hours", "minutes", "seconds"]:
|
||||
period = remainders[x]
|
||||
if period > 0:
|
||||
rv += "{} {}, ".format(period, x[:-1] if period == 1 else x)
|
||||
return rv[:-2]
|
||||
|
||||
|
||||
def td_human_months_weeks(remainders):
|
||||
rv = ""
|
||||
for x in ["years", "months", "weeks_months", "days_weeks_months", "hours", "minutes", "seconds"]:
|
||||
period = remainders[x]
|
||||
if period > 0:
|
||||
rv += "{} {}, ".format(period, x.split('_')[0][:-1] if period == 1 else x.split('_')[0])
|
||||
return rv[:-2]
|
||||
|
||||
|
||||
def td_totals(td):
|
||||
seconds = td.total_seconds()
|
||||
minutes = seconds / 60
|
||||
|
@ -3,7 +3,7 @@ import unittest
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from tww import query_to_format_result, setup_logging_level
|
||||
from tww.tokenizer import main, setup_logging_level
|
||||
|
||||
|
||||
def get_local_hours_offset():
|
||||
@ -22,7 +22,7 @@ UTC = -get_local_hours_offset()
|
||||
|
||||
# LocalTimezone, Query, Result
|
||||
INTEGRATION_TESTS = [
|
||||
(UTC, "now", "2015-03-14 09:26:53"),
|
||||
(UTC, "now", ["2015-03-14 09:26:53"]),
|
||||
# in two hours -> 11:26 | to tokyo +9 -> 20:26
|
||||
(0, "in two hours to asia/tokyo", "2015-03-14 20:26:53"),
|
||||
# now in UTC +2hrs -> 11:26
|
||||
@ -38,12 +38,13 @@ class IntegrationTests(unittest.TestCase):
|
||||
for test in INTEGRATION_TESTS:
|
||||
local_tz = test[0]
|
||||
query = test[1].split(" ")
|
||||
args = type("false_args", (), {"query": query, "locale": None, "full": False, "handlers": False})
|
||||
result = test[2]
|
||||
|
||||
freezer = freeze_time(FROZEN_TIME, tz_offset=local_tz)
|
||||
|
||||
freezer.start()
|
||||
self.assertEqual(query_to_format_result(query), result)
|
||||
self.assertEqual(main(args), result)
|
||||
freezer.stop()
|
||||
|
||||
|
||||
|
@ -10,7 +10,7 @@ from scalpl import Cut
|
||||
|
||||
from tww.localization import setlocale, resolve_locale
|
||||
from tww.lib import ISO_FORMAT, time_to_emoji, time_ago, workday_diff, workhours_diff, td_remainders, td_totals, \
|
||||
td_iso8601, ISO_Z_FORMAT
|
||||
td_iso8601, ISO_Z_FORMAT, td_human, td_machine, tzinfo_from_offset, td_human_months_weeks
|
||||
from tww.lib import resolve_timezone, dateparser_parse_dt, get_utcnow, get_s_since_epoch, get_ms_since_epoch, \
|
||||
dt_tz_translation, get_local_now, query_to_format_result
|
||||
from tww.common import logger
|
||||
@ -35,6 +35,8 @@ r_workhours_since = re.compile('(?:workhours|work hours)\s*since\s*(.*)', flags=
|
||||
r_workhours_until = re.compile('(?:workhours|work hours)\s*until\s*(.*)', flags=re.IGNORECASE)
|
||||
r_workhours_between = re.compile('(?:workhours|work hours)\s*between\s*(.*)\s*and\s*(.*)', flags=re.IGNORECASE)
|
||||
r_timezone_translation = re.compile('(.*)?\s(?:in|to)\s(.*)', flags=re.IGNORECASE)
|
||||
r_timezone_translation_in_to = re.compile('(.*)(?:in)\s(.*)\s(?:to)\s(.*)', flags=re.IGNORECASE)
|
||||
r_hour_minute_timezone = re.compile('((?:[0[0-9]|1[0-9]|2[0-3]):[0-5][0-9])?\s(.*)', flags=re.IGNORECASE)
|
||||
r_timezone = re.compile('(.*)\s(?:timezone|timezones|tz)', flags=re.IGNORECASE)
|
||||
r_timezone_2 = re.compile('(?:timezone in|timezones in|tz in|timezone|timezones|tz)\s(.*)?', flags=re.IGNORECASE)
|
||||
|
||||
@ -91,23 +93,45 @@ def handler_workhours_diff(start_dt, end_dt) -> dict:
|
||||
diff=td_pretty(diff))
|
||||
|
||||
|
||||
def handler_workhours_since_until(start_dt_s: str) -> dict:
|
||||
def handler_workhours_since_until(start_dt_s: str):
|
||||
return handler_workhours_diff(dateparser_parse_dt(start_dt_s), get_local_now())
|
||||
|
||||
|
||||
def handler_timezone_translation(dt_s: str, timezone_like_s: str) -> dict:
|
||||
def timezone_mangle(dt_s: str, timezone_like_s: str) -> dict:
|
||||
logger.debug("Timezone translation between {} | {}".format(dt_s, timezone_like_s))
|
||||
if dt_s.lower().strip() == "time":
|
||||
dt_s = "now"
|
||||
src_dt = dateparser_parse_dt(dt_s)
|
||||
logger.debug("Source time: {}".format(src_dt))
|
||||
tz = resolve_timezone(timezone_like_s)
|
||||
logger.debug("Destination timezone: {}".format(tz))
|
||||
if not tz:
|
||||
tz, dst_dt = {}, src_dt
|
||||
else:
|
||||
offset = tz.get('tz_offset')
|
||||
dst_dt = dt_tz_translation(src_dt, offset)
|
||||
return src_dt, offset, tz
|
||||
|
||||
|
||||
def handler_timezone_creation(dt_s: str, timezone_like_s: str):
|
||||
src_dt, offset, tz = timezone_mangle(dt_s, timezone_like_s)
|
||||
tzinfo = tzinfo_from_offset(offset)
|
||||
dst_dt = tzinfo[0].localize(src_dt.replace(tzinfo=None))
|
||||
return src_dt, dst_dt, tz
|
||||
|
||||
|
||||
def handler_timezone_translation(dt_s: str, timezone_like_s: str):
|
||||
src_dt, offset, tz = timezone_mangle(dt_s, timezone_like_s)
|
||||
dst_dt = dt_tz_translation(src_dt, offset)
|
||||
return src_dt, dst_dt, tz
|
||||
|
||||
|
||||
def handler_timezone_translation_in_to(dt_s: str, timezone_like_s: str, timezone_like_d: str):
|
||||
_, src_dt, _ = handler_timezone_creation(dt_s, timezone_like_s)
|
||||
_, dst_offset, dst_tz = timezone_mangle(dt_s, timezone_like_d)
|
||||
dst_dt = dt_tz_translation(src_dt, dst_offset)
|
||||
return src_dt, dst_dt, dst_tz
|
||||
|
||||
|
||||
def handler_generic_parser(dt_s: str) -> datetime:
|
||||
return dateparser_parse_dt(dt_s)
|
||||
|
||||
@ -132,7 +156,7 @@ h_tz_offset = 'tz->tz_offset'
|
||||
h_time_in = 'dt->hh:mm'
|
||||
h_translation = 'dt->iso8601_full'
|
||||
h_default_dt = 'dt->iso8601_full'
|
||||
h_default_td = 'timedelta->diff->duration_iso8601'
|
||||
h_default_td = 'timedelta->diff->duration_human'
|
||||
|
||||
regex_handlers = [
|
||||
(r_time_in_epoch_s_now, handler_time_now_local, QUERY_TYPE_DT, h_unix_s),
|
||||
@ -144,6 +168,7 @@ regex_handlers = [
|
||||
(r_time_in_epoch_ms2, handler_time, QUERY_TYPE_DT, h_unix_ms),
|
||||
(r_time_in_epoch_ms3, handler_time, QUERY_TYPE_DT, h_unix_ms),
|
||||
(r_timezone_translation, handler_timezone_translation, QUERY_TYPE_DT_TR, h_translation),
|
||||
(r_timezone_translation_in_to, handler_timezone_translation_in_to, QUERY_TYPE_DT_TR, h_translation),
|
||||
(r_time_since, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
|
||||
(r_time_until, handler_time_since_until, QUERY_TYPE_TD, h_default_td),
|
||||
(r_time_between, handler_time_diff, QUERY_TYPE_TD, h_default_td),
|
||||
@ -156,6 +181,7 @@ regex_handlers = [
|
||||
(r_time_in, handler_time_in_parser, QUERY_TYPE_DT, h_time_in),
|
||||
(r_timezone, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
|
||||
(r_timezone_2, handler_timezone, QUERY_TYPE_TZ, h_tz_offset),
|
||||
(r_hour_minute_timezone, handler_timezone_creation, QUERY_TYPE_DT_TR, h_translation),
|
||||
(r_generic, handler_generic_parser, QUERY_TYPE_DT, h_default_dt),
|
||||
]
|
||||
|
||||
@ -173,13 +199,18 @@ def try_regex(r, s):
|
||||
def tokenize(s):
|
||||
solutions = []
|
||||
for r, h, t, hi in regex_handlers:
|
||||
logger.debug("Trying regex: {}".format(r))
|
||||
g = try_regex(r, s)
|
||||
if g is not None:
|
||||
try:
|
||||
logger.debug("Matched regex: {}".format(r))
|
||||
logger.debug("Running handler: {} | query_type: {} | output_type: {}".format(h.__name__, t, hi))
|
||||
result = h(*g)
|
||||
except Exception as e:
|
||||
logger.debug("Exception from handler: {} -> {}".format(h.__name__, e))
|
||||
continue
|
||||
if result is not None:
|
||||
logger.debug("Matched regex: {}".format(r))
|
||||
solutions.append((h.__name__, result, t, hi))
|
||||
return solutions
|
||||
|
||||
@ -191,6 +222,7 @@ def pretty_print_dict(obj):
|
||||
|
||||
|
||||
def show_magic_results(obj, args):
|
||||
rv = []
|
||||
for solution in obj['solutions']:
|
||||
entry_proxy = Cut(solution, sep='->')
|
||||
highlight_entry = solution["highlight"]
|
||||
@ -199,10 +231,12 @@ def show_magic_results(obj, args):
|
||||
except Exception as e:
|
||||
continue
|
||||
if args.handlers:
|
||||
print("{} -> {}".format(solution['handler'], highlight_result))
|
||||
to_print = "{} -> {}".format(solution['handler'], highlight_result)
|
||||
else:
|
||||
print(highlight_result)
|
||||
|
||||
to_print = highlight_result
|
||||
rv.append(to_print)
|
||||
print(to_print)
|
||||
return rv
|
||||
|
||||
def dt_pretty(dt):
|
||||
rv = {}
|
||||
@ -212,7 +246,6 @@ def dt_pretty(dt):
|
||||
from_tz_offset=dt.strftime('%z')).strftime(ISO_Z_FORMAT)
|
||||
rv["iso8601_date"] = dt.strftime('%Y-%m-%d')
|
||||
rv["iso8601_time"] = dt.strftime('%H:%M:%S')
|
||||
rv["locale_dt"] = dt.strftime("%c")
|
||||
rv["day_of_week_number"] = dt.strftime("%w")
|
||||
rv["locale"] = custom_locale
|
||||
with setlocale(locale.LC_TIME, custom_locale.get("lc_time")):
|
||||
@ -222,6 +255,7 @@ def dt_pretty(dt):
|
||||
rv["locale_day_of_week"] = dt.strftime("%A")
|
||||
rv["locale_date"] = dt.strftime("%x")
|
||||
rv["locale_time"] = dt.strftime("%X")
|
||||
rv["locale_dt"] = dt.strftime("%c")
|
||||
rv["tz_offset"] = dt.strftime("%z")
|
||||
rv["hh:mm"] = dt.strftime("%H:%M")
|
||||
rv["emoji_time"] = time_to_emoji(dt)
|
||||
@ -231,13 +265,17 @@ def dt_pretty(dt):
|
||||
|
||||
|
||||
def td_pretty(td):
|
||||
remainders = td_remainders(td)
|
||||
rv = {
|
||||
"sign": '-' if td.days > 0 else '+',
|
||||
"in_the": 'future' if td.days < 0 else 'past',
|
||||
"time_ago": time_ago(td),
|
||||
"duration_iso8601": td_iso8601(td),
|
||||
"duration_machine": td_machine(remainders),
|
||||
"duration_human": td_human(remainders),
|
||||
"duration_human_months_weeks": td_human_months_weeks(remainders),
|
||||
"totals": td_totals(td),
|
||||
"remainders": td_remainders(td),
|
||||
"remainders": remainders,
|
||||
}
|
||||
return rv
|
||||
|
||||
@ -331,9 +369,11 @@ def setup_logging_level(debug=False):
|
||||
def main(args):
|
||||
global custom_locale
|
||||
custom_locale = resolve_locale(args.locale)
|
||||
if custom_locale:
|
||||
logger.debug("Locale understood as: {}".format(custom_locale))
|
||||
query = ' '.join(args.query)
|
||||
# query = "tz in sofia"
|
||||
result = resolve_query(query)
|
||||
if args.full:
|
||||
pretty_print_dict(result)
|
||||
show_magic_results(result, args)
|
||||
return show_magic_results(result, args)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user