def write_ride_photo_primary(strava_activity, ride): """ Store primary photo for activity from the main detail-level activity. :param strava_activity: The Strava :class:`stravalib.orm.Activity` object. :type strava_activity: :class:`stravalib.orm.Activity` :param ride: The db model object for ride. :type ride: bafs.orm.Ride """ # If we have > 1 instagram photo, then we don't do anything. if strava_activity.photo_count > 1: log.debug("Ignoring basic sync for {} since there are > 1 instagram photos.") return # Start by removing any priamry photos for this ride. meta.engine.execute(RidePhoto.__table__.delete().where(and_(RidePhoto.ride_id == strava_activity.id, RidePhoto.primary == True))) primary_photo = strava_activity.photos.primary if primary_photo: if primary_photo.source == 1: _write_strava_photo_primary(primary_photo, ride) else: _write_instagram_photo_primary(primary_photo, ride)
def index(): page = int(request.args.get('page', 1)) if page < 1: page = 1 page_size = 60 offset = page_size * (page - 1) limit = page_size log.debug("Page = {0}, offset={1}, limit={2}".format(page, offset, limit)) total_q = meta.session_factory().query(RidePhoto).join(Ride).order_by(Ride.start_date.desc()) num_photos = total_q.count() page_q = total_q.limit(limit).offset(offset) if num_photos < offset: page = 1 total_pages = int(math.ceil( (1.0 * num_photos) / page_size)) if page > total_pages: page = total_pages return render_template('photos.html', photos=page_q, page=page, total_pages=total_pages)
def write_ride_photo_primary(strava_activity, ride): """ Store primary photo for activity from the main detail-level activity. :param strava_activity: The Strava :class:`stravalib.orm.Activity` object. :type strava_activity: :class:`stravalib.orm.Activity` :param ride: The db model object for ride. :type ride: bafs.orm.Ride """ # If we have > 1 instagram photo, then we don't do anything. if strava_activity.photo_count > 1: log.debug( "Ignoring basic sync for {} since there are > 1 instagram photos.") return # Start by removing any priamry photos for this ride. meta.engine.execute(RidePhoto.__table__.delete().where( and_(RidePhoto.ride_id == strava_activity.id, RidePhoto.primary == True))) primary_photo = strava_activity.photos.primary if primary_photo: if primary_photo.source == 1: _write_strava_photo_primary(primary_photo, ride) else: _write_instagram_photo_primary(primary_photo, ride)
def update_ride_from_activity(strava_activity, ride): """ Refactoring to just set ride properties from the Strava Activity object. :param strava_activity: The Strava Activyt :type strava_activity: stravalib.orm.Activity :param ride: The ride model object. :type ride: Ride """ # Should apply to both new and preexisting rides ... # If there are multiple instagram photos, then request syncing of non-primary photos too. if strava_activity.photo_count > 1 and ride.photos_fetched is None: log.debug("Scheduling non-primary photos sync for {!r}".format(ride)) ride.photos_fetched = False ride.private = bool(strava_activity.private) ride.name = strava_activity.name ride.start_date = strava_activity.start_date_local # We need to round so that "1.0" miles in strava is "1.0" miles when we convert back from meters. ride.distance = round(float(unithelper.miles(strava_activity.distance)), 3) ride.average_speed = float(unithelper.mph(strava_activity.average_speed)) ride.maximum_speed = float(unithelper.mph(strava_activity.max_speed)) ride.elapsed_time = timedelta_to_seconds(strava_activity.elapsed_time) ride.moving_time = timedelta_to_seconds(strava_activity.moving_time) location_parts = [] if strava_activity.location_city: location_parts.append(strava_activity.location_city) if strava_activity.location_state: location_parts.append(strava_activity.location_state) location_str = ', '.join(location_parts) ride.location = location_str ride.commute = strava_activity.commute ride.trainer = strava_activity.trainer ride.manual = strava_activity.manual ride.elevation_gain = float( unithelper.feet(strava_activity.total_elevation_gain)) ride.timezone = str(strava_activity.timezone) # # Short-circuit things that might result in more obscure db errors later. if ride.elapsed_time is None: raise DataEntryError("Activities cannot have null elapsed time.") if ride.moving_time is None: raise DataEntryError("Activities cannot have null moving time.") if ride.distance is None: raise DataEntryError("Activities cannot have null distance.") log.debug("Writing ride for {athlete!r}: \"{ride!r}\" on {date}".format( athlete=ride.athlete.name, ride=ride.name, date=ride.start_date.strftime('%m/%d/%y')))
def update_ride_from_activity(strava_activity, ride): """ Refactoring to just set ride properties from the Strava Activity object. :param strava_activity: The Strava Activyt :type strava_activity: stravalib.orm.Activity :param ride: The ride model object. :type ride: Ride """ # Should apply to both new and preexisting rides ... # If there are multiple instagram photos, then request syncing of non-primary photos too. if strava_activity.photo_count > 1 and ride.photos_fetched is None: log.debug("Scheduling non-primary photos sync for {!r}".format(ride)) ride.photos_fetched = False ride.private = bool(strava_activity.private) ride.name = strava_activity.name ride.start_date = strava_activity.start_date_local # We need to round so that "1.0" miles in strava is "1.0" miles when we convert back from meters. ride.distance = round(float(unithelper.miles(strava_activity.distance)), 3) ride.average_speed = float(unithelper.mph(strava_activity.average_speed)) ride.maximum_speed = float(unithelper.mph(strava_activity.max_speed)) ride.elapsed_time = timedelta_to_seconds(strava_activity.elapsed_time) ride.moving_time = timedelta_to_seconds(strava_activity.moving_time) location_parts = [] if strava_activity.location_city: location_parts.append(strava_activity.location_city) if strava_activity.location_state: location_parts.append(strava_activity.location_state) location_str = ', '.join(location_parts) ride.location = location_str ride.commute = strava_activity.commute ride.trainer = strava_activity.trainer ride.manual = strava_activity.manual ride.elevation_gain = float(unithelper.feet(strava_activity.total_elevation_gain)) ride.timezone = str(strava_activity.timezone) # # Short-circuit things that might result in more obscure db errors later. if ride.elapsed_time is None: raise DataEntryError("Activities cannot have null elapsed time.") if ride.moving_time is None: raise DataEntryError("Activities cannot have null moving time.") if ride.distance is None: raise DataEntryError("Activities cannot have null distance.") log.debug("Writing ride for {athlete!r}: \"{ride!r}\" on {date}".format(athlete=ride.athlete.name, ride=ride.name, date=ride.start_date.strftime('%m/%d/%y')))
def disambiguate_athlete_display_names(): q = meta.scoped_session().query(orm.Athlete) q = q.filter(orm.Athlete.access_token != None) athletes = q.all() # Ok, here is the plan; bin these things together based on firstname and last initial. # Then iterate over each bin and if there are multiple entries, find the least number # of letters to make them all different. (we should be able to use set intersection # to check for differences within the bins?) def firstlast(name): name_parts = a.name.split(' ') fname = name_parts[0] if len(name_parts) < 2: lname = None else: lname = name_parts[-1] return (fname, lname) athletes_bin = {} for a in athletes: (fname, lname) = firstlast(a.name) if lname is None: # We only care about people with first and last names for this exercise # key = fname continue else: key = '{0} {1}'.format(fname, lname[0]) athletes_bin.setdefault(key, []).append(a) for (name_key, athletes) in athletes_bin.items(): shortest_lname = min([firstlast(a.name)[1] for a in athletes], key=len) required_length = None for i in range(len(shortest_lname)): # Calculate fname + lname-of-x-chars for each athlete. # If unique, then use this number and update the model objects candidate_short_lasts = [firstlast(a.name)[1][:i + 1] for a in athletes] if len(set(candidate_short_lasts)) == len(candidate_short_lasts): required_length = i + 1 break if required_length is not None: for a in athletes: fname, lname = firstlast(a.name) log.debug("Converting '{fname} {lname}' -> '{fname} {minlname}".format(fname=fname, lname=lname, minlname=lname[ :required_length])) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) else: log.debug("Unable to find a minimum lastname; using full lastname.") # Just use full names for a in athletes: fname, lname = firstlast(a.name) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) # Update the database with new values meta.scoped_session().commit()
def _geo_tracks(start_date=None, end_date=None, team_id=None): # These dates must be made naive, since we don't have TZ info stored in our ride columns. if start_date: start_date = arrow.get(start_date).datetime.replace(tzinfo=None) if end_date: end_date = arrow.get(end_date).datetime.replace(tzinfo=None) log.debug("Filtering on start_date: {}".format(start_date)) log.debug("Filtering on end_date: {}".format(end_date)) sess = meta.scoped_session() q = sess.query(RideTrack).join(Ride).join(Athlete) q = q.filter(Ride.private==False) if team_id: q = q.filter(Athlete.team_id==team_id) if start_date: q = q.filter(Ride.start_date >= start_date) if end_date: q = q.filter(Ride.start_date < end_date) linestrings = [] for ride_track in q: assert isinstance(ride_track, RideTrack) ride_tz = pytz.timezone(ride_track.ride.timezone) wkt = sess.scalar(ride_track.gps_track.wkt) coordinates = [] for (i, (lon, lat)) in enumerate(parse_linestring(wkt)): elapsed_time = ride_track.ride.start_date + timedelta(seconds=ride_track.time_stream[i]) point = ( float(Decimal(lon)), float(Decimal(lat)), float(Decimal(ride_track.elevation_stream[i])), ride_tz.localize(elapsed_time).isoformat() ) coordinates.append(point) linestrings.append(coordinates) geojson_structure = {"type": "MultiLineString", "coordinates": linestrings} #return geojson.dumps(geojson.MultiLineString(linestrings)) return json.dumps(geojson_structure)
def _geo_tracks(start_date=None, end_date=None, team_id=None): # These dates must be made naive, since we don't have TZ info stored in our ride columns. if start_date: start_date = arrow.get(start_date).datetime.replace(tzinfo=None) if end_date: end_date = arrow.get(end_date).datetime.replace(tzinfo=None) log.debug("Filtering on start_date: {}".format(start_date)) log.debug("Filtering on end_date: {}".format(end_date)) sess = meta.scoped_session() q = sess.query(RideTrack).join(Ride).join(Athlete) q = q.filter(Ride.private == False) if team_id: q = q.filter(Athlete.team_id == team_id) if start_date: q = q.filter(Ride.start_date >= start_date) if end_date: q = q.filter(Ride.start_date < end_date) linestrings = [] for ride_track in q: assert isinstance(ride_track, RideTrack) ride_tz = pytz.timezone(ride_track.ride.timezone) wkt = sess.scalar(ride_track.gps_track.wkt) coordinates = [] for (i, (lon, lat)) in enumerate(parse_linestring(wkt)): elapsed_time = ride_track.ride.start_date + timedelta( seconds=ride_track.time_stream[i]) point = (float(Decimal(lon)), float(Decimal(lat)), float(Decimal(ride_track.elevation_stream[i])), ride_tz.localize(elapsed_time).isoformat()) coordinates.append(point) linestrings.append(coordinates) geojson_structure = {"type": "MultiLineString", "coordinates": linestrings} #return geojson.dumps(geojson.MultiLineString(linestrings)) return json.dumps(geojson_structure)
def _write_strava_photo_primary(photo, ride): """ Writes a strava native (source=1) primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.orm.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.orm.Ride :return: The newly added ride photo object. :rtype: bafs.orm.RidePhoto """ # 'photos': {u'count': 1, # u'primary': {u'id': None, # u'source': 1, # u'unique_id': u'35453b4b-0fc1-46fd-a824-a4548426b57d', # u'urls': {u'100': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-128x96.jpg', # u'600': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-768x576.jpg'}}, # u'use_primary_photo': False}, if not photo.urls: log.warning( "Photo {} present, but has no URLs (skipping)".format(photo)) return None p = RidePhoto() p.id = photo.unique_id p.primary = True p.source = photo.source p.ref = None p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id log.debug("Writing (primary) Strava ride photo: {}".format(p)) meta.scoped_session().add(p) meta.scoped_session().flush() return p
def write_ride_efforts(strava_activity, ride): """ Writes out all effort associated with a ride to the database. :param strava_activity: The :class:`stravalib.orm.Activity` that is associated with this effort. :type strava_activity: :class:`stravalib.orm.Activity` :param ride: The db model object for ride. :type ride: :class:`bafs.orm.Ride` """ assert isinstance(strava_activity, strava_model.Activity) assert isinstance(ride, Ride) try: # Start by removing any existing segments for the ride. meta.engine.execute(RideEffort.__table__.delete().where( RideEffort.ride_id == strava_activity.id)) # Then add them back in for se in strava_activity.segment_efforts: effort = RideEffort(id=se.id, ride_id=strava_activity.id, elapsed_time=timedelta_to_seconds( se.elapsed_time), segment_name=se.segment.name, segment_id=se.segment.id) log.debug("Writing ride effort: {se_id}: {effort!r}".format( se_id=se.id, effort=effort.segment_name)) meta.scoped_session().add(effort) meta.scoped_session().flush() ride.efforts_fetched = True except: log.exception("Error adding effort for ride: {0}".format(ride)) raise
def _write_strava_photo_primary(photo, ride): """ Writes a strava native (source=1) primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.orm.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.orm.Ride :return: The newly added ride photo object. :rtype: bafs.orm.RidePhoto """ # 'photos': {u'count': 1, # u'primary': {u'id': None, # u'source': 1, # u'unique_id': u'35453b4b-0fc1-46fd-a824-a4548426b57d', # u'urls': {u'100': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-128x96.jpg', # u'600': u'https://dgtzuqphqg23d.cloudfront.net/Vvm_Mcfk1SP-VWdglQJImBvKzGKRJrHlNN4BqAqD1po-768x576.jpg'}}, # u'use_primary_photo': False}, if not photo.urls: log.warning("Photo {} present, but has no URLs (skipping)".format(photo)) return None p = RidePhoto() p.id = photo.unique_id p.primary = True p.source = photo.source p.ref = None p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id log.debug("Writing (primary) Strava ride photo: {}".format(p)) meta.scoped_session().add(p) meta.scoped_session().flush() return p
def write_ride_efforts(strava_activity, ride): """ Writes out all effort associated with a ride to the database. :param strava_activity: The :class:`stravalib.orm.Activity` that is associated with this effort. :type strava_activity: :class:`stravalib.orm.Activity` :param ride: The db model object for ride. :type ride: :class:`bafs.orm.Ride` """ assert isinstance(strava_activity, strava_model.Activity) assert isinstance(ride, Ride) try: # Start by removing any existing segments for the ride. meta.engine.execute(RideEffort.__table__.delete().where(RideEffort.ride_id == strava_activity.id)) # Then add them back in for se in strava_activity.segment_efforts: effort = RideEffort(id=se.id, ride_id=strava_activity.id, elapsed_time=timedelta_to_seconds(se.elapsed_time), segment_name=se.segment.name, segment_id=se.segment.id) log.debug("Writing ride effort: {se_id}: {effort!r}".format(se_id=se.id, effort=effort.segment_name)) meta.scoped_session().add(effort) meta.scoped_session().flush() ride.efforts_fetched = True except: log.exception("Error adding effort for ride: {0}".format(ride)) raise
def _write_instagram_photo_primary(photo, ride): """ Writes an instagram primary photo to db. :param photo: The primary photo from an activity. :type photo: stravalib.orm.ActivityPhotoPrimary :param ride: The db model object for ride. :type ride: bafs.orm.Ride :return: The newly added ride photo object. :rtype: bafs.orm.RidePhoto """ # Here is when we have an Instagram photo as primary: # u'photos': {u'count': 1, # u'primary': {u'id': 106409096, # u'source': 2, # u'unique_id': None, # u'urls': {u'100': u'https://instagram.com/p/88qaqZvrBI/media?size=t', # u'600': u'https://instagram.com/p/88qaqZvrBI/media?size=l'}}, # u'use_prima ry_photo': False}, media = None # This doesn't work any more; Instagram changed their API to use OAuth. #insta_client = insta.configured_instagram_client() #shortcode = re.search(r'/p/([^/]+)/', photo.urls['100']).group(1) # try: # #log.debug("Fetching Instagram media for shortcode: {}".format(shortcode)) # media = insta_client.media_shortcode(shortcode) # except (InstagramAPIError, InstagramClientError) as e: # if e.status_code == 400: # log.warning("Instagram photo {} for ride {}; user is set to private".format(shortcode, ride)) # elif e.status_code == 404: # log.warning("Photo {} for ride {}; shortcode not found".format(shortcode, ride)) # else: # log.exception("Error fetching instagram photo {}".format(photo)) p = RidePhoto() if media: p.id = media.id p.ref = media.link p.img_l = media.get_standard_resolution_url() p.img_t = media.get_thumbnail_url() if media.caption: p.caption = media.caption.text else: p.id = photo.id p.ref = re.match(r'(.+/)media\?size=.$', photo.urls['100']).group(1) p.img_l = photo.urls['600'] p.img_t = photo.urls['100'] p.ride_id = ride.id p.primary = True p.source = photo.source log.debug("Writing (primary) Instagram ride photo: {!r}".format(p)) meta.scoped_session().add(p) meta.scoped_session().flush() return p
def disambiguate_athlete_display_names(): q = meta.scoped_session().query(orm.Athlete) q = q.filter(orm.Athlete.access_token != None) athletes = q.all() # Ok, here is the plan; bin these things together based on firstname and last initial. # Then iterate over each bin and if there are multiple entries, find the least number # of letters to make them all different. (we should be able to use set intersection # to check for differences within the bins?) def firstlast(name): name_parts = a.name.split(' ') fname = name_parts[0] if len(name_parts) < 2: lname = None else: lname = name_parts[-1] return (fname, lname) athletes_bin = {} for a in athletes: (fname, lname) = firstlast(a.name) if lname is None: # We only care about people with first and last names for this exercise # key = fname continue else: key = '{0} {1}'.format(fname, lname[0]) athletes_bin.setdefault(key, []).append(a) for (name_key, athletes) in athletes_bin.items(): shortest_lname = min([firstlast(a.name)[1] for a in athletes], key=len) required_length = None for i in range(len(shortest_lname)): # Calculate fname + lname-of-x-chars for each athlete. # If unique, then use this number and update the model objects candidate_short_lasts = [ firstlast(a.name)[1][:i + 1] for a in athletes ] if len(set(candidate_short_lasts)) == len(candidate_short_lasts): required_length = i + 1 break if required_length is not None: for a in athletes: fname, lname = firstlast(a.name) log.debug( "Converting '{fname} {lname}' -> '{fname} {minlname}". format(fname=fname, lname=lname, minlname=lname[:required_length])) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) else: log.debug( "Unable to find a minimum lastname; using full lastname.") # Just use full names for a in athletes: fname, lname = firstlast(a.name) a.display_name = '{0} {1}'.format(fname, lname[:required_length]) # Update the database with new values meta.scoped_session().commit()
def write_ride_photos_nonprimary(activity_photos, ride): """ Writes out non-primary photos (currently only instagram) associated with a ride to the database. :param activity_photos: Photos for an activity. :type activity_photos: list[stravalib.orm.ActivityPhoto] :param ride: The db model object for ride. :type ride: bafs.orm.Ride """ # [{u'activity_id': 414980300, # u'activity_name': u'Pimmit Run CX', # u'caption': u'Pimmit Run cx', # u'created_at': u'2015-10-17T20:51:02Z', # u'created_at_local': u'2015-10-17T16:51:02Z', # u'id': 106409096, # u'ref': u'https://instagram.com/p/88qaqZvrBI/', # u'resource_state': 2, # u'sizes': {u'0': [150, 150]}, # u'source': 2, # u'type': u'InstagramPhoto', # u'uid': u'1097938959360503880_297644011', # u'unique_id': None, # u'uploaded_at': u'2015-10-17T17:55:45Z', # u'urls': {u'0': u'https://instagram.com/p/88qaqZvrBI/media?size=t'}}] meta.engine.execute(RidePhoto.__table__.delete().where( and_(RidePhoto.ride_id == ride.id, RidePhoto.primary == False))) insta_client = insta.configured_instagram_client() for activity_photo in activity_photos: # If it's already in the db, then skip it. existing = meta.scoped_session().query(RidePhoto).get( activity_photo.uid) if existing: log.info("Skipping photo {} because it's already in database: {}". format(activity_photo, existing)) continue try: media = insta_client.media(activity_photo.uid) photo = RidePhoto(id=activity_photo.uid, ride_id=ride.id, ref=activity_photo.ref, caption=activity_photo.caption) photo.img_l = media.get_standard_resolution_url() photo.img_t = media.get_thumbnail_url() meta.scoped_session().add(photo) log.debug( "Writing (non-primary) ride photo: {p_id}: {photo!r}".format( p_id=photo.id, photo=photo)) meta.scoped_session().flush() except (InstagramAPIError, InstagramClientError) as e: if e.status_code == 400: log.warning( "Skipping photo {0} for ride {1}; user is set to private". format(activity_photo, ride)) elif e.status_code == 404: log.warning( "Skipping photo {0} for ride {1}; not found".format( activity_photo, ride)) else: log.exception( "Error fetching instagram photo {0} (skipping)".format( activity_photo)) ride.photos_fetched = True
def write_ride_photos_nonprimary(activity_photos, ride): """ Writes out non-primary photos (currently only instagram) associated with a ride to the database. :param activity_photos: Photos for an activity. :type activity_photos: list[stravalib.orm.ActivityPhoto] :param ride: The db model object for ride. :type ride: bafs.orm.Ride """ # [{u'activity_id': 414980300, # u'activity_name': u'Pimmit Run CX', # u'caption': u'Pimmit Run cx', # u'created_at': u'2015-10-17T20:51:02Z', # u'created_at_local': u'2015-10-17T16:51:02Z', # u'id': 106409096, # u'ref': u'https://instagram.com/p/88qaqZvrBI/', # u'resource_state': 2, # u'sizes': {u'0': [150, 150]}, # u'source': 2, # u'type': u'InstagramPhoto', # u'uid': u'1097938959360503880_297644011', # u'unique_id': None, # u'uploaded_at': u'2015-10-17T17:55:45Z', # u'urls': {u'0': u'https://instagram.com/p/88qaqZvrBI/media?size=t'}}] meta.engine.execute(RidePhoto.__table__.delete().where(and_(RidePhoto.ride_id == ride.id, RidePhoto.primary == False))) insta_client = insta.configured_instagram_client() for activity_photo in activity_photos: # If it's already in the db, then skip it. existing = meta.scoped_session().query(RidePhoto).get(activity_photo.uid) if existing: log.info("Skipping photo {} because it's already in database: {}".format(activity_photo, existing)) continue try: media = insta_client.media(activity_photo.uid) photo = RidePhoto(id=activity_photo.uid, ride_id=ride.id, ref=activity_photo.ref, caption=activity_photo.caption) photo.img_l = media.get_standard_resolution_url() photo.img_t = media.get_thumbnail_url() meta.scoped_session().add(photo) log.debug("Writing (non-primary) ride photo: {p_id}: {photo!r}".format(p_id=photo.id, photo=photo)) meta.scoped_session().flush() except (InstagramAPIError, InstagramClientError) as e: if e.status_code == 400: log.warning("Skipping photo {0} for ride {1}; user is set to private".format(activity_photo, ride)) elif e.status_code == 404: log.warning("Skipping photo {0} for ride {1}; not found".format(activity_photo, ride)) else: log.exception("Error fetching instagram photo {0} (skipping)".format(activity_photo)) ride.photos_fetched = True