source: OpenWorkouts-current/ow/utilities.py @ 76ebb1b

currentfeature/docs
Last change on this file since 76ebb1b was 76ebb1b, checked in by Borja Lopez <borja@…>, 5 years ago

(#29) Add user verification by email on signup.

From now on, when a new user signs up, we set the account into an "unverified"
state. In order to complete the signup procedure, the user has to click on a
link we send by email to the email address provided on signup.

IMPORTANT: A new dependency has been added, pyramid_mailer, so remember to
install it in any existing openworkouts environment (this is done automatically
if you use the ./bin/start script):

pip install pyramid_mailer

  • Property mode set to 100644
File size: 8.7 KB
RevLine 
[5ec3a0b]1import re
[d1c4782]2import os
3import logging
[9ab0fe3]4import calendar
[d6f8304]5import shutil
6import time
[2f8a48f]7from datetime import datetime, timedelta
[53bb3e5]8from decimal import Decimal
9from shutil import copyfileobj
[76ebb1b]10from uuid import uuid4
[53bb3e5]11
[5ec3a0b]12from unidecode import unidecode
13from xml.dom import minidom
[53bb3e5]14from ZODB.blob import Blob
[d6f8304]15from splinter import Browser
16
[d517001]17from pyramid.i18n import TranslationStringFactory
18
19_ = TranslationStringFactory('OpenWorkouts')
20
[5ec3a0b]21
[d1c4782]22log = logging.getLogger(__name__)
23
[5ec3a0b]24
[76ebb1b]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
[5ec3a0b]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:
[53bb3e5]110                    t = datetime.strptime(
[5ec3a0b]111                        rfc3339, '%Y-%m-%dT%H:%M:%S.%fZ')
112                except ValueError:
[53bb3e5]113                    t = datetime.strptime(
[5ec3a0b]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})
[53bb3e5]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
[119412d]200def create_blob(data, file_extension, binary=False):
[53bb3e5]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
[119412d]208        if not binary and not isinstance(data, bytes):
[53bb3e5]209            data = data.encode('utf-8')
210        open_blob.write(data)
211    return blob
[d1c4782]212
213
[d6f8304]214def save_map_screenshot(workout, request):
215
[d1c4782]216    if workout.has_gpx:
217
[d6f8304]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__))
[d1c4782]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
[d6f8304]243        shutil.move(splinter_screenshot_path, screenshot_path)
[02aee97]244        os.chmod(screenshot_path, 0o644)
[d1c4782]245        return True
246
247    return False
[2f8a48f]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
[9ab0fe3]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    return None
[d517001]282
283
284def part_of_day(dt):
285    """
286    Given a datetime object (dt), return which part of the day was it
287    (morning, afternoon, evening, night), translated in the proper
288    """
289    parts = {
290        _('Morning'): (5, 11),
291        _('Afternoon'): (12, 17),
292        _('Evening'): (18, 22),
293        _('Night'): (23, 4)
294    }
295    for key, value in parts.items():
296        if value[0] <= dt.hour <= value[1]:
297            return key
Note: See TracBrowser for help on using the repository browser.