source: OpenWorkouts-current/ow/utilities.py @ 8c2b094

current
Last change on this file since 8c2b094 was 24596da, checked in by Borja Lopez <borja@…>, 5 years ago

Added missing tests, raised overall coverage

  • Property mode set to 100644
File size: 10.3 KB
Line 
1import re
2import os
3import logging
4import calendar
5import shutil
6import time
7from datetime import datetime, timedelta
8from decimal import Decimal
9from shutil import copyfileobj
10from uuid import uuid4
11
12from unidecode import unidecode
13from xml.dom import minidom
14from ZODB.blob import Blob
15from splinter import Browser
16
17from pyramid.i18n import TranslationStringFactory
18
19_ = TranslationStringFactory('OpenWorkouts')
20
21
22log = logging.getLogger(__name__)
23
24
25def get_verification_token():
26    """
27    Generate a new uuid4 verification token we can give a user for
28    verification purposes.
29    uuid4 is a standard that generates a randomly generated token,
30    optimized for a very low chance of collisions. But even if
31    we had a collision, it wouldn't matter - it's simple some users
32    getting the same token in their verification mail.
33    """
34    return uuid4()
35
36
37def slugify(text, delim=u'-'):
38    """
39    Generates an ASCII-only slug.
40    from http://flask.pocoo.org/snippets/5/
41    """
42    _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
43    result = []
44    text = unidecode(text)
45    for word in _punct_re.split(text.lower()):
46        result.extend(word.split())
47    return delim.join(result)
48
49
50class GPXMinidomParser(object):
51    """
52    GPX parser, using minidom from the base library.
53
54    We need this as a workaround, as gpxpy does not handle GPX 1.1 extensions
55    correctly right now (and we have not been able to fix it).
56
57    This method is inspired by this blog post:
58
59    http://castfortwo.blogspot.com.au/2014/06/
60    parsing-strava-gpx-file-with-python.html
61    """
62
63    def __init__(self, gpx_path):
64        self.gpx_path = gpx_path
65        self.gpx = None
66        self.tracks = {}
67
68    def load_gpx(self):
69        """
70        Load the given gpx file into a minidom doc, normalize it and set
71        self.gpx to the document root so we can reuse it later on
72        """
73        doc = minidom.parse(self.gpx_path)
74        doc.normalize()
75        self.gpx = doc.documentElement
76
77    def parse_tracks(self):
78        """
79        Loop over all the tracks found in the gpx, parsing them
80        """
81        for trk in self.gpx.getElementsByTagName('trk'):
82            self.parse_track(trk)
83
84    def parse_track(self, trk):
85        """
86        Parse the given track, extracting all the information and putting it
87        into a dict where the key is the track name and the value is a list
88        of data for the the different segments and points in the track.
89
90        All the data is saved in self.tracks
91        """
92        name = trk.getElementsByTagName('name')[0].firstChild.data
93        if name not in self.tracks:
94            self.tracks[name] = []
95
96        for trkseg in trk.getElementsByTagName('trkseg'):
97            for trkpt in trkseg.getElementsByTagName('trkpt'):
98                lat = Decimal(trkpt.getAttribute('lat'))
99                lon = Decimal(trkpt.getAttribute('lon'))
100
101                # There could happen there is no elevation data
102                ele = trkpt.getElementsByTagName('ele')
103                if ele:
104                    ele = Decimal(ele[0].firstChild.data)
105                else:
106                    ele = None
107
108                rfc3339 = trkpt.getElementsByTagName('time')[0].firstChild.data
109                try:
110                    t = datetime.strptime(
111                        rfc3339, '%Y-%m-%dT%H:%M:%S.%fZ')
112                except ValueError:
113                    t = datetime.strptime(
114                        rfc3339, '%Y-%m-%dT%H:%M:%SZ')
115
116                hr = None
117                cad = None
118                atemp = None
119                extensions = trkpt.getElementsByTagName('extensions')
120                if extensions:
121                    extensions = extensions[0]
122                    trkPtExt = extensions.getElementsByTagName(
123                        'gpxtpx:TrackPointExtension')[0]
124                    if trkPtExt:
125                        hr_ext = trkPtExt.getElementsByTagName('gpxtpx:hr')
126                        cad_ext = trkPtExt.getElementsByTagName('gpxtpx:cad')
127                        atemp_ext = trkPtExt.getElementsByTagName(
128                            'gpxtpx:atemp')
129                        if hr_ext:
130                            hr = Decimal(hr_ext[0].firstChild.data)
131                        if cad_ext:
132                            cad = Decimal(cad_ext[0].firstChild.data)
133                        if atemp_ext:
134                            atemp = Decimal(atemp_ext[0].firstChild.data)
135
136                self.tracks[name].append({
137                    'lat': lat,
138                    'lon': lon,
139                    'ele': ele,
140                    'time': t,
141                    'hr': hr,
142                    'cad': cad,
143                    'atemp': atemp})
144
145
146def semicircles_to_degrees(semicircles):
147    return semicircles * (180 / pow(2, 31))
148
149
150def degrees_to_semicircles(degrees):
151    return degrees * (pow(2, 31) / 180)
152
153
154def miles_to_kms(miles):
155    factor = 0.62137119
156    return miles / factor
157
158
159def kms_to_miles(kms):
160    factor = 0.62137119
161    return kms * factor
162
163
164def meters_to_kms(meters):
165    return meters / 1000
166
167
168def kms_to_meters(kms):
169    return kms * 1000
170
171
172def mps_to_kmph(mps):
173    """
174    Transform a value from meters-per-second to kilometers-per-hour
175    """
176    return mps * 3.6
177
178
179def kmph_to_mps(kmph):
180    """
181    Transform a value from kilometers-per-hour to meters-per-second
182    """
183    return kmph * 0.277778
184
185
186def copy_blob(blob):
187    """
188    Create a copy of a blob object, returning another blob object that is
189    the copy of the given blob file.
190    """
191    new_blob = Blob()
192    if getattr(blob, 'file_extension', None):
193        new_blob.file_extension = blob.file_extension
194    with blob.open('r') as orig_blob, new_blob.open('w') as dest_blob:
195        orig_blob.seek(0)
196        copyfileobj(orig_blob, dest_blob)
197    return new_blob
198
199
200def create_blob(data, file_extension, binary=False):
201    """
202    Create a ZODB blob file from some data, return the blob object
203    """
204    blob = Blob()
205    blob.file_extension = file_extension
206    with blob.open('w') as open_blob:
207        # use .encode() to convert the string to bytes if needed
208        if not binary and not isinstance(data, bytes):
209            data = data.encode('utf-8')
210        open_blob.write(data)
211    return blob
212
213
214def save_map_screenshot(workout, request):
215
216    if workout.has_gpx:
217
218        map_url = request.resource_url(workout, 'map')
219
220        browser = Browser('chrome', headless=True)
221        browser.driver.set_window_size(1300, 436)
222
223        browser.visit(map_url)
224        # we need to wait a moment before taking the screenshot, to ensure
225        # all tiles are loaded in the map.
226        time.sleep(5)
227
228        # splinter saves the screenshot with a random name (even if we do
229        # provide a name) so we get the path to that file and later on we
230        # move it to the proper place
231        splinter_screenshot_path = browser.screenshot()
232
233        current_path = os.path.abspath(os.path.dirname(__file__))
234        screenshots_path = os.path.join(
235            current_path, 'static/maps', str(workout.owner.uid))
236        if not os.path.exists(screenshots_path):
237            os.makedirs(screenshots_path)
238
239        screenshot_path = os.path.join(
240            screenshots_path, str(workout.workout_id))
241        screenshot_path += '.png'
242
243        shutil.move(splinter_screenshot_path, screenshot_path)
244        os.chmod(screenshot_path, 0o644)
245        return True
246
247    return False
248
249
250def timedelta_to_hms(value):
251    """
252    Return hours, minutes, seconds from a timedelta object
253    """
254    hours, remainder = divmod(int(value.total_seconds()), 3600)
255    minutes, seconds = divmod(remainder, 60)
256    return hours, minutes, seconds
257
258
259def get_week_days(day, start_day=1):
260    """
261    Return a list of datetime objects for the days of the week "day" is in.
262
263    day is a datetime object (like in datetime.now() for "today")
264
265    start_day can be used to set if week starts on monday (1) or sunday (0)
266    """
267    first_day = day - timedelta(days=day.isoweekday() - start_day)
268    week_days = [first_day + timedelta(days=i) for i in range(7)]
269    return week_days
270
271
272def get_month_week_number(day):
273    """
274    Given a datetime object (day), return the number of week the day is
275    in the current month (week 1, 2, 3, etc)
276    """
277    weeks = calendar.monthcalendar(day.year, day.month)
278    for week in weeks:
279        if day.day in week:
280            return weeks.index(week)
281
282
283def get_month_names():
284    """
285    Return a list with the names of the months, marked for translation.
286
287    This should be done automatically by the calendar module:
288
289    >>> import calendar
290    >>> calendar.month_name[1]
291    'January'
292    >>>
293
294    But even trying setting the proper locale (using locale.setlocale()),
295    in some operating systems the names are not translated (OpenBSD).
296
297    So, for now, we use this dirty trick
298    """
299    return [
300        '',
301        _('January'),
302        _('February'),
303        _('March'),
304        _('April'),
305        _('May'),
306        _('June'),
307        _('July'),
308        _('August'),
309        _('September'),
310        _('October'),
311        _('November'),
312        _('December')
313    ]
314
315
316def get_week_day_names():
317    """
318    Return a list with the names of the week days, marked for translation.
319
320    As with get_month_names(), this is a dirty workaround for some locale
321    problem in some operating systems
322    """
323    return [
324        _('Monday'),
325        _('Tuesday'),
326        _('Wednesday'),
327        _('Thursday'),
328        _('Friday'),
329        _('Saturday'),
330        _('Sunday'),
331    ]
332
333
334def part_of_day(dt):
335    """
336    Given a datetime object (dt), return which part of the day was it
337    (morning, afternoon, evening, night), translated in the proper
338    """
339    parts = {
340        _('Morning'): (5, 11),
341        _('Afternoon'): (12, 17),
342        _('Evening'): (18, 22),
343        _('Night'): (23, 4)
344    }
345    for key, value in parts.items():
346        if value[0] <= dt.hour <= value[1]:
347            return key
348
349
350def get_available_locale_names():
351    """
352    Return a list of tuples with info about available locale/language
353    names.
354
355    The locale codes and names in this list match the available translations
356    under ow/locale for the UI elements
357    """
358    return [
359        ('en', _('English')),
360        ('es', _('Spanish'))
361    ]
362
363
364def get_gender_names():
365    return [
366        ('male', _('Male')),
367        ('female', _('Female')),
368        ('robot', _('Robot'))
369    ]
Note: See TracBrowser for help on using the repository browser.