2016-11-08 12:56:19 +01:00
|
|
|
import datetime
|
2016-08-19 09:19:06 +02:00
|
|
|
import logging
|
|
|
|
import traceback
|
|
|
|
import sys
|
2017-03-03 14:14:36 +01:00
|
|
|
import typing
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-08 13:38:36 +01:00
|
|
|
import dateutil.parser
|
2016-08-19 09:19:06 +02:00
|
|
|
from flask import current_app
|
|
|
|
from flask import request
|
2016-10-11 15:23:40 +02:00
|
|
|
from flask_login import current_user
|
2016-08-19 09:19:06 +02:00
|
|
|
from pillarsdk import File
|
|
|
|
from pillarsdk import Project
|
|
|
|
from pillarsdk.exceptions import ResourceNotFound
|
|
|
|
import pillarsdk.utils
|
|
|
|
from pillar.web import system_util
|
|
|
|
from pillar.web.utils.exceptions import ConfigError
|
|
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
def get_file(file_id, api=None):
|
|
|
|
# TODO: remove this function and just use the Pillar SDK directly.
|
|
|
|
if file_id is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if api is None:
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
|
|
|
|
try:
|
|
|
|
return File.find(file_id, api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
f = sys.exc_info()[2].tb_frame.f_back
|
|
|
|
tb = traceback.format_stack(f=f, limit=2)
|
|
|
|
log.warning('File %s not found, but requested from %s\n%s',
|
|
|
|
file_id, request.url, ''.join(tb))
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def attach_project_pictures(project, api):
|
|
|
|
"""Utility function that queries for file objects referenced in picture
|
|
|
|
header and square. In eve we currently can't embed objects in nested
|
|
|
|
properties, this is the reason why this exists.
|
|
|
|
This function should be moved in the API, attached to a new Project object.
|
|
|
|
"""
|
|
|
|
|
2019-05-22 10:14:19 +02:00
|
|
|
# When adding to the list of pictures dealt with here, make sure
|
|
|
|
# you update unattach_project_pictures() too.
|
2016-08-19 09:19:06 +02:00
|
|
|
project.picture_square = get_file(project.picture_square, api=api)
|
|
|
|
project.picture_header = get_file(project.picture_header, api=api)
|
2019-04-19 14:10:19 +02:00
|
|
|
project.picture_16_9 = get_file(project.picture_16_9, api=api)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
|
2019-05-22 10:14:19 +02:00
|
|
|
def unattach_project_pictures(project: dict):
|
|
|
|
"""Reverts the operation of 'attach_project_pictures'.
|
|
|
|
|
|
|
|
This makes it possible to PUT the project again.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def unattach(property_name: str):
|
|
|
|
picture_info = project.get(property_name, None)
|
|
|
|
if not picture_info:
|
|
|
|
project.pop(property_name, None)
|
|
|
|
return
|
|
|
|
|
|
|
|
if not isinstance(picture_info, dict):
|
|
|
|
# Assume it's already is an ID.
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
picture_id = picture_info['_id']
|
|
|
|
project[property_name] = picture_id
|
|
|
|
except KeyError:
|
|
|
|
return
|
|
|
|
|
|
|
|
unattach('picture_square')
|
|
|
|
unattach('picture_header')
|
|
|
|
unattach('picture_16_9')
|
|
|
|
|
|
|
|
|
2018-01-30 15:52:55 +01:00
|
|
|
def mass_attach_project_pictures(projects: typing.Iterable[pillarsdk.Project], *,
|
|
|
|
api, header=True, square=True):
|
|
|
|
"""Attach file object to all projects in the list.
|
|
|
|
|
|
|
|
Queries for all picture files at once and sets the header and square
|
|
|
|
images.
|
|
|
|
"""
|
|
|
|
|
|
|
|
my_log = log.getChild('mass_attach_project_pictures')
|
|
|
|
|
|
|
|
if not (header or square):
|
|
|
|
raise ValueError("at least one of header/square must be True")
|
|
|
|
|
|
|
|
if not projects:
|
|
|
|
return
|
|
|
|
|
|
|
|
file_ids = set()
|
|
|
|
if header:
|
|
|
|
file_ids.update(p.picture_header for p in projects if p.picture_header)
|
|
|
|
if square:
|
|
|
|
file_ids.update(p.picture_square for p in projects if p.picture_square)
|
|
|
|
|
|
|
|
if not file_ids:
|
|
|
|
return
|
|
|
|
|
|
|
|
fid_list = list(file_ids)
|
|
|
|
my_log.debug('mass-fetching %d files %s', len(fid_list), fid_list)
|
|
|
|
|
|
|
|
file_resp = File.all({'where': {'_id': {'$in': fid_list}}}, api=api)
|
|
|
|
file_obs = {f['_id']: f for f in file_resp['_items']}
|
|
|
|
|
|
|
|
to_find = len(file_ids)
|
|
|
|
found = 0
|
|
|
|
missing = 0
|
|
|
|
for p in projects:
|
|
|
|
if header and p.picture_header:
|
|
|
|
try:
|
|
|
|
p.picture_header = file_obs[p.picture_header]
|
|
|
|
except KeyError:
|
|
|
|
p.picture_header = None
|
|
|
|
my_log.warning('File %r not found, but used as picture_header in project %s',
|
|
|
|
p.picture_header, p['_id'])
|
|
|
|
missing += 1
|
|
|
|
else:
|
|
|
|
found += 1
|
|
|
|
if square and p.picture_square:
|
|
|
|
try:
|
|
|
|
p.picture_square = file_obs[p.picture_square]
|
|
|
|
except KeyError:
|
|
|
|
p.picture_square = None
|
|
|
|
my_log.warning('File %s not found, but used as picture_square in project %s',
|
|
|
|
p.picture_square, p['_id'])
|
|
|
|
missing += 1
|
|
|
|
else:
|
|
|
|
found += 1
|
|
|
|
my_log.debug('found %d of %d pictures, there were %d missing', found, to_find, missing)
|
|
|
|
|
|
|
|
|
2017-03-03 14:42:13 +01:00
|
|
|
def gravatar(email: str, size=64):
|
2019-05-24 17:36:06 +02:00
|
|
|
"""Deprecated: return the Gravatar URL.
|
|
|
|
|
|
|
|
.. deprecated::
|
|
|
|
Use of Gravatar is deprecated, in favour of our self-hosted avatars.
|
|
|
|
See pillar.api.users.avatar.url(user).
|
|
|
|
"""
|
2017-03-03 14:42:13 +01:00
|
|
|
import warnings
|
2019-05-24 17:36:06 +02:00
|
|
|
warnings.warn('pillar.web.utils.gravatar() is deprecated, '
|
|
|
|
'use pillar.api.users.avatar.url() instead',
|
|
|
|
category=DeprecationWarning, stacklevel=2)
|
2017-03-03 14:42:13 +01:00
|
|
|
|
|
|
|
from pillar.api.utils import gravatar as api_gravatar
|
|
|
|
return api_gravatar(email, size)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
|
2016-11-08 12:56:19 +01:00
|
|
|
def pretty_date(time, detail=False, now=None):
|
2016-08-19 09:19:06 +02:00
|
|
|
"""Get a datetime object or a int() Epoch timestamp and return a
|
|
|
|
pretty string like 'an hour ago', 'Yesterday', '3 months ago',
|
|
|
|
'just now', etc
|
|
|
|
"""
|
|
|
|
|
2016-11-08 12:56:19 +01:00
|
|
|
if time is None:
|
|
|
|
return None
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
# Normalize the 'time' parameter so it's always a datetime.
|
|
|
|
if type(time) is int:
|
2016-11-08 12:24:50 +01:00
|
|
|
time = datetime.datetime.fromtimestamp(time, tz=pillarsdk.utils.utc)
|
2017-03-03 12:00:24 +01:00
|
|
|
elif isinstance(time, str):
|
2016-11-08 13:38:36 +01:00
|
|
|
time = dateutil.parser.parse(time)
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-08 12:24:50 +01:00
|
|
|
now = now or datetime.datetime.now(tz=time.tzinfo)
|
|
|
|
diff = now - time # TODO: flip the sign, so that future = positive and past = negative.
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-08 12:24:50 +01:00
|
|
|
second_diff = diff.seconds # Always positive, so -1 second = -1 day + 23h59m59s
|
2016-08-19 09:19:06 +02:00
|
|
|
day_diff = diff.days
|
|
|
|
|
2016-11-08 12:24:50 +01:00
|
|
|
if day_diff < 0 and time.year != now.year:
|
|
|
|
# "16 Jul 2018"
|
|
|
|
pretty = time.strftime("%d %b %Y")
|
|
|
|
|
|
|
|
elif day_diff < -21 and time.year == now.year:
|
|
|
|
# "16 Jul"
|
|
|
|
pretty = time.strftime("%d %b")
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-08 12:24:50 +01:00
|
|
|
elif day_diff < -7:
|
|
|
|
week_count = -day_diff // 7
|
|
|
|
if week_count == 1:
|
|
|
|
pretty = "in 1 week"
|
|
|
|
else:
|
|
|
|
pretty = "in %s weeks" % week_count
|
|
|
|
|
|
|
|
elif day_diff < -1:
|
|
|
|
# "next Tuesday"
|
|
|
|
pretty = 'next %s' % time.strftime("%A")
|
|
|
|
elif day_diff == -1:
|
|
|
|
# Compute the actual number of seconds in the future, positively.
|
|
|
|
seconds = 24 * 3600 - second_diff
|
|
|
|
if seconds < 10:
|
|
|
|
return 'just now'
|
|
|
|
if seconds < 60:
|
|
|
|
return 'in %ss' % seconds
|
|
|
|
if seconds < 120:
|
|
|
|
return 'in a minute'
|
|
|
|
if seconds < 3600:
|
|
|
|
return 'in %im' % (seconds // 60)
|
|
|
|
if seconds < 7200:
|
|
|
|
return 'in an hour'
|
|
|
|
if seconds < 86400:
|
|
|
|
return 'in %ih' % (seconds // 3600)
|
|
|
|
elif day_diff == 0:
|
2016-08-19 09:19:06 +02:00
|
|
|
if second_diff < 10:
|
|
|
|
return "just now"
|
|
|
|
if second_diff < 60:
|
|
|
|
return str(second_diff) + "s ago"
|
|
|
|
if second_diff < 120:
|
|
|
|
return "a minute ago"
|
|
|
|
if second_diff < 3600:
|
2016-11-08 12:24:50 +01:00
|
|
|
return str(second_diff // 60) + "m ago"
|
2016-08-19 09:19:06 +02:00
|
|
|
if second_diff < 7200:
|
|
|
|
return "an hour ago"
|
|
|
|
if second_diff < 86400:
|
2016-11-08 12:24:50 +01:00
|
|
|
return str(second_diff // 3600) + "h ago"
|
2016-08-19 09:19:06 +02:00
|
|
|
|
2016-11-08 12:24:50 +01:00
|
|
|
elif day_diff == 1:
|
2016-08-19 09:19:06 +02:00
|
|
|
pretty = "yesterday"
|
|
|
|
|
|
|
|
elif day_diff <= 7:
|
2016-11-08 12:24:50 +01:00
|
|
|
# "last Tuesday"
|
|
|
|
pretty = 'last %s' % time.strftime("%A")
|
2016-08-19 09:19:06 +02:00
|
|
|
|
|
|
|
elif day_diff <= 22:
|
2016-11-08 12:24:50 +01:00
|
|
|
week_count = day_diff // 7
|
2016-08-19 09:19:06 +02:00
|
|
|
if week_count == 1:
|
2016-11-08 12:06:40 +01:00
|
|
|
pretty = "1 week ago"
|
2016-08-19 09:19:06 +02:00
|
|
|
else:
|
|
|
|
pretty = "%s weeks ago" % week_count
|
|
|
|
|
|
|
|
elif time.year == now.year:
|
|
|
|
# "16 Jul"
|
|
|
|
pretty = time.strftime("%d %b")
|
|
|
|
|
|
|
|
else:
|
|
|
|
# "16 Jul 2009"
|
|
|
|
pretty = time.strftime("%d %b %Y")
|
|
|
|
|
|
|
|
if detail:
|
|
|
|
# "Tuesday at 14:20"
|
|
|
|
return '%s at %s' % (pretty, time.strftime('%H:%M'))
|
|
|
|
|
|
|
|
return pretty
|
|
|
|
|
|
|
|
|
|
|
|
def current_user_is_authenticated():
|
|
|
|
return current_user.is_authenticated
|
|
|
|
|
|
|
|
|
|
|
|
def get_main_project():
|
|
|
|
api = system_util.pillar_api()
|
|
|
|
try:
|
|
|
|
main_project = Project.find(
|
|
|
|
current_app.config['MAIN_PROJECT_ID'], api=api)
|
|
|
|
except ResourceNotFound:
|
|
|
|
raise ConfigError('MAIN_PROJECT_ID was not found. Check config.py.')
|
|
|
|
except KeyError:
|
|
|
|
raise ConfigError('MAIN_PROJECT_ID missing from config.py')
|
|
|
|
return main_project
|
2016-08-24 14:49:30 +02:00
|
|
|
|
|
|
|
|
2017-03-03 14:14:36 +01:00
|
|
|
def is_valid_id(some_id: typing.Union[str, bytes]):
|
2016-08-24 14:49:30 +02:00
|
|
|
"""Returns True iff the given string is a valid ObjectId.
|
|
|
|
|
|
|
|
Only use this if you do NOT need an ObjectId object. If you do need that,
|
|
|
|
use pillar.api.utils.str2id() instead.
|
|
|
|
|
|
|
|
:type some_id: unicode
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
|
|
|
|
2017-03-03 14:14:36 +01:00
|
|
|
if isinstance(some_id, bytes):
|
|
|
|
return len(some_id) == 12
|
|
|
|
|
2017-03-03 12:00:24 +01:00
|
|
|
if not isinstance(some_id, str):
|
2016-08-24 14:49:30 +02:00
|
|
|
return False
|
|
|
|
|
2017-03-03 14:14:36 +01:00
|
|
|
if len(some_id) != 24:
|
|
|
|
return False
|
|
|
|
|
|
|
|
# This is more than 5x faster than checking character by
|
|
|
|
# character in a loop.
|
|
|
|
try:
|
|
|
|
int(some_id, 16)
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
return True
|
2017-01-19 15:13:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
def last_page_index(meta_info):
|
|
|
|
"""Eve pagination; returns the index of the last page.
|
|
|
|
|
|
|
|
:param meta_info: Eve's '_meta' response.
|
|
|
|
:returns: Eve page number (base-1) of the last page.
|
|
|
|
:rtype: int
|
|
|
|
"""
|
|
|
|
|
|
|
|
total = meta_info['total']
|
|
|
|
if total == 0:
|
|
|
|
return 1
|
|
|
|
|
|
|
|
per_page = meta_info['max_results']
|
|
|
|
|
|
|
|
pages = total // per_page
|
|
|
|
if total % per_page == 0:
|
|
|
|
return pages
|
|
|
|
|
|
|
|
return pages + 1
|