Skip to content

Commit b947751

Browse files
authored
Feature/geolocation (#193)
1 parent f85eb84 commit b947751

File tree

12 files changed

+225
-31
lines changed

12 files changed

+225
-31
lines changed

.DS_Store

6 KB
Binary file not shown.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,7 @@ app/routers/stam
162162
.idea
163163

164164
junit/
165+
166+
# .DS_Store
167+
.DS_Store
168+
DS_Store

app/database/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ class Event(Base):
9292
end = Column(DateTime, nullable=False)
9393
content = Column(String)
9494
location = Column(String, nullable=True)
95+
latitude = Column(String, nullable=True)
96+
longitude = Column(String, nullable=True)
9597
vc_link = Column(String, nullable=True)
9698
is_google_event = Column(Boolean, default=False)
9799
color = Column(String, nullable=True)

app/internal/event.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
import logging
22
import re
3-
from typing import List, Set
3+
from typing import List, NamedTuple, Set, Union
44

55
from email_validator import EmailSyntaxError, validate_email
66
from fastapi import HTTPException
7+
from geopy.adapters import AioHTTPAdapter
8+
from geopy.exc import GeocoderTimedOut, GeocoderUnavailable
9+
from geopy.geocoders import Nominatim
10+
from loguru import logger
711
from sqlalchemy.orm import Session
812
from starlette.status import HTTP_400_BAD_REQUEST
913

1014
from app.database.models import Event
1115

16+
1217
ZOOM_REGEX = re.compile(r"https://.*?\.zoom.us/[a-z]/.[^.,\b\s]+")
1318

1419

20+
class Location(NamedTuple):
21+
# Location type hint class.
22+
latitude: str
23+
longitude: str
24+
name: str
25+
26+
1527
def raise_if_zoom_link_invalid(vc_link):
1628
if ZOOM_REGEX.search(vc_link) is None:
1729
raise HTTPException(
@@ -101,3 +113,27 @@ def get_messages(
101113
f"Want to create another one {weeks_diff} after too?",
102114
)
103115
return messages
116+
117+
118+
async def get_location_coordinates(
119+
address: str,
120+
) -> Union[Location, str]:
121+
"""Return location coordinates and accurate
122+
address of the specified location."""
123+
try:
124+
async with Nominatim(
125+
user_agent="Pylendar",
126+
adapter_factory=AioHTTPAdapter,
127+
) as geolocator:
128+
geolocation = await geolocator.geocode(address)
129+
except (GeocoderTimedOut, GeocoderUnavailable) as e:
130+
logger.exception(str(e))
131+
else:
132+
if geolocation is not None:
133+
location = Location(
134+
latitude=geolocation.latitude,
135+
longitude=geolocation.longitude,
136+
name=geolocation.raw["display_name"],
137+
)
138+
return location
139+
return address

app/routers/event.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,20 @@
1616

1717
from app.database.models import Comment, Event, User, UserEvent
1818
from app.dependencies import get_db, logger, templates
19+
from app.internal import comment as cmt
20+
from app.internal.emotion import get_emotion
1921
from app.internal.event import (
2022
get_invited_emails,
23+
get_location_coordinates,
2124
get_messages,
2225
get_uninvited_regular_emails,
2326
raise_if_zoom_link_invalid,
2427
)
25-
from app.internal import comment as cmt
26-
from app.internal.emotion import get_emotion
2728
from app.internal.privacy import PrivacyKinds
2829
from app.internal.utils import create_model, get_current_user
2930
from app.routers.categories import get_user_categories
3031

32+
3133
EVENT_DATA = Tuple[Event, List[Dict[str, str]], str]
3234
TIME_FORMAT = "%Y-%m-%d %H:%M"
3335
START_FORMAT = "%A, %d/%m/%Y %H:%M"
@@ -132,9 +134,16 @@ async def create_new_event(
132134
title,
133135
invited_emails,
134136
)
137+
latitude, longitude = None, None
135138

136139
if vc_link:
137140
raise_if_zoom_link_invalid(vc_link)
141+
else:
142+
location_details = await get_location_coordinates(location)
143+
if not isinstance(location_details, str):
144+
location = location_details.name
145+
latitude = location_details.latitude
146+
longitude = location_details.longitude
138147

139148
event = create_event(
140149
db=session,
@@ -145,6 +154,8 @@ async def create_new_event(
145154
owner_id=owner_id,
146155
content=content,
147156
location=location,
157+
latitude=latitude,
158+
longitude=longitude,
148159
vc_link=vc_link,
149160
invitees=invited_emails,
150161
category_id=category_id,
@@ -411,6 +422,8 @@ def create_event(
411422
content: Optional[str] = None,
412423
location: Optional[str] = None,
413424
vc_link: str = None,
425+
latitude: Optional[str] = None,
426+
longitude: Optional[str] = None,
414427
color: Optional[str] = None,
415428
invitees: List[str] = None,
416429
category_id: Optional[int] = None,
@@ -432,6 +445,8 @@ def create_event(
432445
content=content,
433446
owner_id=owner_id,
434447
location=location,
448+
latitude=latitude,
449+
longitude=longitude,
435450
vc_link=vc_link,
436451
color=color,
437452
emotion=get_emotion(title, content),

app/static/event/eventview.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@ div.event_info_row,
5050
margin-block-end: 0.2em;
5151
}
5252

53-
.title {
54-
border-bottom: 4px solid blue;
55-
}
56-
5753
.title h1 {
5854
white-space: nowrap;
5955
margin-block-start: 0.2em;
@@ -72,4 +68,8 @@ div.event_info_row,
7268

7369
button {
7470
height: 100%;
75-
}
71+
}
72+
73+
.google_maps_object {
74+
width: 100%;
75+
}

app/templates/partials/calendar/event/view_event_details_tab.html

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div class="event_info_row title" style="border-bottom: 4px solid {{ event.color }}">
2-
<div class="event_info_row_start">
3-
<h1>{{ event.title }}</h1>
4-
</div>
5-
<div class="event_info_row_end">
6-
<!-- <span class="icon">AVAILABILITY</span>-->
7-
<!-- <span class="icon">PRIVACY</span>-->
8-
</div>
2+
<div class="event_info_row_start">
3+
<h1>{{ event.title }}</h1>
4+
</div>
5+
<div class="event_info_row_end">
6+
<!-- <span class="icon">AVAILABILITY</span>-->
7+
<!-- <span class="icon">PRIVACY</span>-->
8+
</div>
99
</div>
1010
<div class="event_info_row">
1111
<span class="icon">ICON</span>
@@ -35,24 +35,39 @@ <h1>{{ event.title }}</h1>
3535
<address>VC link<a href="{{ event.vc_link }}">VC URL</a></address>
3636
</div>
3737
{% endif %}
38+
39+
{% if event.latitude is not none and event.longitude is not none %}
40+
<div class="google_maps_object">
41+
<iframe
42+
width="600"
43+
height="400"
44+
frameborder="0"
45+
scrolling="no"
46+
marginheight="0"
47+
marginwidth="0"
48+
src="https://maps.google.com/maps?q={{event.latitude}},{{event.longitude}}&hl=es;z=14&amp;output=embed">
49+
</iframe>
50+
</div>
51+
{% endif %}
52+
3853
{% if event.invitees %}
3954
<form method="POST" action="/event/{{event.id}}/owner">
40-
<div class="mb-3"></div>
41-
<label for="username">{{ gettext('Choose user') }}</label>
42-
<select id="username" name="username">
43-
{% for username in event.invitees.split(',') %}
44-
<option value="{{username}}">{{username}}</option>
45-
{% endfor %}
46-
</select>
47-
<input type="submit" class="btn btn-primary" value="Change owner">
55+
<div class="mb-3"></div>
56+
<label for="username">{{ gettext('Choose user') }}</label>
57+
<select id="username" name="username">
58+
{% for username in event.invitees.split(',') %}
59+
<option value="{{username}}">{{username}}</option>
60+
{% endfor %}
61+
</select>
62+
<input type="submit" class="btn btn-primary" value="Change owner">
4863
</form>
4964
{% endif %}
5065
<p class="event_info_row">
51-
{{event.owner.username}}
66+
{{event.owner.username}}
5267
</p>
5368

5469
<div class="event_info_buttons_row event_info_row_end">
55-
<!-- Buttons could and should be replaced with button-like anchors if need so -->
56-
<button type="button">Duplicate</button>
57-
<button type="button">Edit</button>
70+
<!-- Buttons could and should be replaced with button-like anchors if need so -->
71+
<button type="button">Duplicate</button>
72+
<button type="button">Edit</button>
5873
</div>

requirements.txt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
aiofiles==0.6.0
2+
aiohttp==3.7.3
23
aioredis==1.3.1
34
aiosmtpd==1.2.2
45
aiosmtplib==1.1.4
@@ -24,7 +25,7 @@ cachetools==4.2.0
2425
certifi==2020.12.5
2526
cffi==1.14.4
2627
cfgv==3.2.0
27-
chardet==4.0.0
28+
chardet==3.0.4
2829
click==7.1.2
2930
colorama==0.4.4
3031
coverage==5.3.1
@@ -44,6 +45,8 @@ fastapi-mail==0.3.3.1
4445
filelock==3.0.12
4546
flake8==3.8.4
4647
frozendict==1.2
48+
geographiclib==1.50
49+
geopy==2.1.0
4750
google-api-core==1.25.0
4851
google-api-python-client==1.12.8
4952
google-auth==1.24.0
@@ -77,13 +80,15 @@ mocker==1.1.1
7780
multidict==5.1.0
7881
mypy==0.790
7982
mypy-extensions==0.4.3
83+
nest-asyncio==1.5.1
8084
nltk==3.5
8185
nodeenv==1.5.0
8286
oauth2client==4.1.3
8387
oauthlib==3.1.0
8488
outcome==1.1.0
8589
packaging==20.8
8690
passlib==1.7.4
91+
pathspec==0.8.1
8792
Pillow==8.1.0
8893
pluggy==0.13.1
8994
pre-commit==2.10.0
@@ -148,4 +153,5 @@ win32-setctime==1.0.3
148153
word-forms==2.1.0
149154
wsproto==1.0.0
150155
yapf==0.30.0
151-
zipp==3.4.0
156+
yarl==1.6.3
157+
zipp==3.4.0

tests/asyncio_fixture.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def fake_user_events(session):
3333
create_event(
3434
db=session,
3535
title='Cool today event',
36+
color='red',
3637
start=today_date,
3738
end=today_date + timedelta(days=2),
3839
all_day=False,
@@ -44,6 +45,7 @@ def fake_user_events(session):
4445
create_event(
4546
db=session,
4647
title='Cool (somewhen in two days) event',
48+
color='blue',
4749
start=today_date + timedelta(days=1),
4850
end=today_date + timedelta(days=3),
4951
all_day=False,

tests/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import calendar
22

3+
import nest_asyncio
34
import pytest
5+
from app.config import PSQL_ENVIRONMENT
6+
from app.database.models import Base
47
from sqlalchemy import create_engine
58
from sqlalchemy.orm import sessionmaker
69

7-
from app.config import PSQL_ENVIRONMENT
8-
from app.database.models import Base
910

1011
pytest_plugins = [
1112
'tests.user_fixture',
@@ -80,3 +81,6 @@ def sqlite_engine():
8081
@pytest.fixture
8182
def Calendar():
8283
return calendar.Calendar(0)
84+
85+
86+
nest_asyncio.apply()

0 commit comments

Comments
 (0)