Skip to content

Commit 6c6f2e2

Browse files
committed
Merged in stage (pull request #26)
release 0.5.0
2 parents 45cb068 + d20df67 commit 6c6f2e2

28 files changed

+705
-185
lines changed

Makefile

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
SHELL = /bin/bash
2+
DOCKER_EXEC_DJANGO=$(shell command -v docker > /dev/null && echo "docker-compose exec django")
3+
DOCKER_EXEC_VUE=$(shell command -v docker > /dev/null && echo "docker-compose exec vue")
4+
DOCKER_EXEC_WWW=$(shell command -v docker > /dev/null && echo "docker-compose exec www")
5+
26
.PHONY: tests
37

48
init:
@@ -17,10 +21,26 @@ push:
1721
cd vue && make push
1822

1923
enter_django:
20-
cd django && make enter
24+
$(DOCKER_EXEC_DJANGO) ash
2125

2226
enter_vue:
23-
cd vue && make enter
27+
$(DOCKER_EXEC_VUE) ash
28+
29+
enter_www:
30+
$(DOCKER_EXEC_WWW) ash
31+
32+
start_vue:
33+
$(DOCKER_EXEC_VUE) make
34+
35+
start_django:
36+
$(DOCKER_EXEC_DJANGO) make
37+
38+
start_screenshotservice:
39+
$(DOCKER_EXEC_VUE) make screenshotservice
40+
41+
reload_www:
42+
$(DOCKER_EXEC_WWW) sh -c 'openresty -t & openresty -s reload'
43+
2444

2545
deploy_prod:
2646
cd vue && make build

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ For a tour of the app and a testing protocol see [`docs/testing.md`](/docs/testi
2424

2525
## setup project
2626

27-
Add `django` to the 127.0.0.1 entry in `/etc/hosts` (for screenshot service).
27+
Add `www.local` and `django` to the 127.0.0.1 entry in `/etc/hosts` (for screenshot service).
2828

2929
```bash
3030
touch env.hosts.prod # required file, can be empty and edited later
@@ -66,6 +66,30 @@ Database files and static generated files are stored in `var` folder.
6666
make up
6767
```
6868

69+
**start services**
70+
71+
```bash
72+
# 1. shell
73+
make start_vue
74+
# 2. shell
75+
make start_django
76+
# 3. shell
77+
make start_screenshotservice
78+
```
79+
80+
You can also start the vue / django service from the vscode shell (see editor).
81+
82+
### tests
83+
84+
```bash
85+
make tests
86+
```
87+
88+
This command runs first the backend tests and afterwards the frontend test.
89+
90+
For the backend tests a new test database is generated and the fixtures are loaded.
91+
92+
6993
### editor
7094

7195
The project is setup to work with visual studio code as editor. Any other editor is also fine.
@@ -101,4 +125,4 @@ Type `make deploy_prod` in the project root. The file `env.hosts.prod` needs to
101125
- [django](/docs/django.md)
102126
- [links](/docs/links.md)
103127
- [grahpql](/docs/graphql.md)
104-
- [troubleshooting](/docs/troubleshooting.md)
128+
- [troubleshooting](/docs/troubleshooting.md)

django/Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
GEMEINDESCAN_WEBUI_DJANGO=0.3.4
22
DOCKER_EXEC=$(shell command -v docker > /dev/null && echo "docker-compose exec django")
3-
all: run-tests run-tests-dev
3+
all: dev run-tests run-tests-dev
44
.PHONY: all
55
default: dev
66

@@ -25,6 +25,9 @@ adminuser:
2525
collectstatic:
2626
$(DOCKER_EXEC) python3 manage.py collectstatic --noinput
2727

28+
symlinks:
29+
$(DOCKER_EXEC) sh -c "cd /var/services/django; ln -s media downloads"
30+
2831
migrate:
2932
$(DOCKER_EXEC) python3 manage.py migrate
3033
make collectstatic
@@ -45,6 +48,7 @@ import-gemeinden-json:
4548
$(DOCKER_EXEC) rm ./tmp/gemeinden.geojson
4649

4750
init:
51+
make symlinks
4852
make migrate
4953
make adminuser
5054
make import-gemeinden-json

django/gsmap/admin.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
from django_json_widget.widgets import JSONEditorWidget
55
from django.utils.html import mark_safe
66
from django.contrib import messages
7+
from django.forms.widgets import Textarea
8+
from django.conf import settings
79
import requests
810
from sortedm2m_filter_horizontal_widget.forms import SortedFilteredSelectMultiple
911
from gsmap.models import Municipality, Snapshot, Workspace
1012

1113

1214
class MunicipalityAdmin(admin.OSMGeoAdmin):
13-
pass
15+
readonly_fields = ('bfs_number',)
16+
fields = ('bfs_number', 'name', 'canton', 'perimeter')
17+
list_display = (
18+
'name',
19+
'bfs_number',
20+
)
21+
list_filter = ('canton',)
22+
search_fields = ('id', 'name', 'canton')
23+
24+
def get_map_widget(self, db_field):
25+
return Textarea
26+
1427

1528

1629
class SnapshotAdmin(admin.OSMGeoAdmin):
@@ -92,7 +105,15 @@ def save_model(self, request, obj, form, change):
92105
try:
93106
obj.save()
94107
except (requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError) as e:
95-
messages.error(request, "Couldn't create the screenshots, screenshot server problem.")
108+
messages.error(
109+
request,
110+
f"Couldn't create the screenshots, screenshot server problem. (ReadTimeout, ConnectionError) {repr(e)}"
111+
)
112+
except Exception as e:
113+
messages.error(
114+
request,
115+
f"Couldn't create the screenshots, screenshot server problem. (Other Error) {repr(e)}"
116+
)
96117

97118

98119
class WorkspaceAdmin(admin.OSMGeoAdmin):
@@ -120,3 +141,4 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs):
120141
admin.site.register(Municipality, MunicipalityAdmin)
121142
admin.site.register(Snapshot, SnapshotAdmin)
122143
admin.site.register(Workspace, WorkspaceAdmin)
144+
admin.site.site_header = settings.ADMIN_NAME

django/gsmap/models.py

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import string
33
from enum import IntFlag
44
import requests
5+
import os
56
from django.utils import timezone
67
from django.db.models.signals import post_save
78
from django.dispatch import receiver
@@ -11,11 +12,21 @@
1112
from django.contrib.sites.models import Site
1213
from django.utils.html import format_html
1314
from django.conf import settings
15+
from django.core.files.base import ContentFile
16+
from django.core.files.storage import FileSystemStorage
1417
from sortedm2m.fields import SortedManyToManyField
15-
from sorl.thumbnail import ImageField
18+
from sorl.thumbnail import ImageField, get_thumbnail
1619
from gsuser.models import User
1720

1821

22+
class OverwriteStorage(FileSystemStorage):
23+
def get_available_name(self, name, max_length=None):
24+
# If the filename already exists, remove it as if it was a true file system
25+
if self.exists(name):
26+
os.remove(os.path.join(settings.MEDIA_ROOT, name))
27+
return name
28+
29+
1930
class Municipality(models.Model):
2031
class Meta:
2132
verbose_name_plural = 'municipalities'
@@ -59,6 +70,10 @@ class Meta:
5970
def fullname(self):
6071
return f'{self.name} ({self.canton})'
6172

73+
@property
74+
def bfs_number(self):
75+
return self.id
76+
6277
def __str__(self):
6378
return self.fullname
6479

@@ -144,10 +159,25 @@ def screenshot(self):
144159
def thumbnail(self):
145160
return self.thumbnail_manual or self.thumbnail_generated
146161

162+
@property
163+
def title_data(self):
164+
try:
165+
return self.data['views'][0]['spec']['title']
166+
except KeyError:
167+
return self.title
168+
169+
@property
170+
def description_data(self):
171+
try:
172+
return self.data['views'][0]['spec']['description']
173+
except KeyError:
174+
return ''
175+
147176
def get_absolute_link(self):
148177
domain = Site.objects.get_current().domain
178+
proto = 'https' if settings.USE_HTTPS else 'http'
149179
return format_html(
150-
f'<a href="//{domain}{self.get_absolute_url()}" target="_blank">'
180+
f'<a href="{proto}://{domain}{self.get_absolute_url()}" target="_blank">'
151181
f'{domain}{self.get_absolute_url()}</a>'
152182
)
153183
get_absolute_link.short_description = "Snapshot Url"
@@ -169,6 +199,28 @@ def create_screenshot_file(self, is_thumbnail=False):
169199
)
170200
return screenshot_file
171201

202+
def image_twitter(self):
203+
if bool(self.screenshot):
204+
return get_thumbnail(
205+
self.screenshot, '1200x630',
206+
crop='bottom', format='PNG'
207+
)
208+
return ''
209+
210+
def image_facebook(self):
211+
if bool(self.screenshot):
212+
return get_thumbnail(
213+
self.screenshot, '1200x630',
214+
crop='bottom', format='PNG'
215+
)
216+
217+
def __str__(self):
218+
if self.municipality:
219+
return f'{self.municipality.fullname}, {self.title}, ' \
220+
f'{self.id} ({self.get_permission_display()})'
221+
else:
222+
return self.title
223+
172224
def save(self, *args, **kwargs):
173225
def test_exists(pk):
174226
if list(self.__class__.objects.filter(pk=pk)):
@@ -181,40 +233,46 @@ def test_exists(pk):
181233
self.id = create_slug_hash_6()
182234
self.id = test_exists(self.id)
183235

236+
if self.data:
237+
storage = OverwriteStorage()
238+
if self.permission is int(SnapshotPermission.PUBLIC):
239+
self.create_meta(storage)
240+
else:
241+
storage.delete(f'snapshot-meta/{self.id}.html')
242+
184243
super().save(*args, **kwargs)
185244

186-
def __str__(self):
187-
if self.municipality:
188-
return f'{self.municipality.fullname}, {self.title}, ' \
189-
f'{self.id} ({self.get_permission_display()})'
190-
else:
191-
return self.title
245+
if hasattr(settings, 'SAVE_SCREENSHOT_ENABLED') and settings.SAVE_SCREENSHOT_ENABLED is True:
246+
self.create_screenshot()
192247

248+
super().save(*args, **kwargs)
193249

194-
@receiver(post_save, sender=Snapshot)
195-
def save_screenshot_handler(sender, **kwargs):
196-
def save_screenshot():
197-
post_save.disconnect(save_screenshot_handler, sender=Snapshot)
198-
instance = kwargs.get('instance')
250+
def create_screenshot(self):
199251
# only create snapshot if data changed
200-
if instance.data_changed([
252+
if self.data_changed([
201253
'data', 'screenshot_generated', 'thumbnail_generated'
202-
]) or not bool(instance.thumbnail_generated):
203-
if not 'resources' in instance.data:
204-
return
205-
try:
206-
# disconnect to break save recursive loop
207-
post_save.disconnect(save_screenshot_handler, sender=Snapshot)
208-
screenshot_file = instance.create_screenshot_file()
209-
thumbnail_file = instance.create_screenshot_file(is_thumbnail=True)
210-
instance.screenshot_generated = screenshot_file
211-
instance.thumbnail_generated = thumbnail_file
212-
instance.save()
213-
finally:
214-
# always reconnect signal
215-
post_save.connect(save_screenshot_handler, sender=Snapshot)
216-
if hasattr(settings, 'SAVE_SCREENSHOT_ENABLED') and settings.SAVE_SCREENSHOT_ENABLED is True:
217-
save_screenshot()
254+
]) or not bool(self.thumbnail_generated):
255+
print('resources', 'resources' in self.data)
256+
if not 'resources' in self.data:
257+
raise ValueError('no resources key in data')
258+
259+
screenshot_file = self.create_screenshot_file()
260+
thumbnail_file = self.create_screenshot_file(is_thumbnail=True)
261+
self.screenshot_generated = screenshot_file
262+
self.thumbnail_generated = thumbnail_file
263+
264+
def create_meta(self, storage):
265+
domain = Site.objects.get_current().domain
266+
proto = 'https' if settings.USE_HTTPS else 'http'
267+
meta = f'''
268+
<meta property="og:title" content="{self.title_data}">
269+
<meta property="og:description" content="{self.description_data}">
270+
<meta property="og:type" content="website">
271+
<meta property="og:url" content="{proto}://{domain}{self.get_absolute_url()}">
272+
<meta property="og:image" content="{proto}://{domain}/{self.image_facebook()}">
273+
<meta name="twitter:image" content="{proto}://{domain}/{self.image_twitter()}">
274+
'''
275+
storage.save(f'snapshot-meta/{self.id}.html', ContentFile(meta))
218276

219277

220278
class Workspace(models.Model):
@@ -234,9 +292,10 @@ class Meta:
234292
snapshots = SortedManyToManyField(Snapshot)
235293

236294
def get_absolute_link(self):
295+
proto = 'https' if settings.USE_HTTPS else 'http'
237296
domain = Site.objects.get_current().domain
238297
return format_html(
239-
f'<a href="//{domain}{self.get_absolute_url()}" target="_blank">'
298+
f'<a href="{proto}://{domain}{self.get_absolute_url()}" target="_blank">'
240299
f'{domain}{self.get_absolute_url()}</a>'
241300
)
242301
get_absolute_link.short_description = "Workspace Url"
@@ -245,7 +304,6 @@ def get_absolute_url(self):
245304
first_id = self.snapshots.all().first().id
246305
return f'/{self.id}/{first_id}/'
247306

248-
249307
def save(self, *args, **kwargs):
250308
def test_exists(pk):
251309
if self.__class__.objects.filter(pk=pk):

django/gsmap/schema.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from graphene_django.types import DjangoObjectType
99
from graphene_django.filter import DjangoFilterConnectionField
1010
from graphene_django.converter import convert_django_field
11-
from sorl.thumbnail import get_thumbnail
1211
from gsmap.models import Municipality, Snapshot, SnapshotPermission, Workspace
1312

1413

@@ -71,18 +70,10 @@ def resolve_thumbnail(self, info):
7170
return self.thumbnail
7271

7372
def resolve_screenshot_facebook(self, info):
74-
screenshot = get_thumbnail(self.screenshot,
75-
'1200x630',
76-
crop='bottom',
77-
format='PNG')
78-
return screenshot
73+
return self.image_facebook()
7974

8075
def resolve_screenshot_twitter(self, info):
81-
screenshot = get_thumbnail(self.screenshot,
82-
'1200x600',
83-
crop='bottom',
84-
format='PNG')
85-
return screenshot
76+
return self.image_twitter()
8677

8778

8879
class MunicipalityNode(DjangoObjectType):

0 commit comments

Comments
 (0)