5 Commits

19 changed files with 86 additions and 250 deletions

3
.gitignore vendored
View File

@@ -95,3 +95,6 @@ venv.bak/
# Editors # Editors
.vscode .vscode
# twine / pypi
.pypirc

View File

@@ -1,5 +1,5 @@
include LICENSE include LICENSE
include README.rst include Readme.rst
include Pipfile include Pipfile
recursive-include lostplaces_app/static * recursive-include lostplaces_app/static *
recursive-include lostplaces_app/templates * recursive-include lostplaces_app/templates *

View File

@@ -9,6 +9,8 @@ coverage = "*"
autopep8 = "*" autopep8 = "*"
pipenv = "*" pipenv = "*"
wheel = "*" wheel = "*"
twine = "*"
pandoc ="*"
[packages] [packages]
django = "*" django = "*"

View File

@@ -4,8 +4,11 @@ lostplaces-backend is a django (3.x) based webproject. It once wants to become a
The software is currently in early development status, neither scope, datalmodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server. The software is currently in early development status, neither scope, datalmodel(s) nor features are finalized yet. Therefore we would not recommend to download or install this piece of software anywhere - except your local django dev server.
We value privacy as a whole, all ressources the frontend requires will be shipped with lostplace's distribution. We also try to minimze the use of JavaScript as far as we can and try to offer JS-less alternatives where we can.
## Features ## Features
- Manage lost places with lots of usefull information. - Manage lost places with lots of usefull information.
- OSM-Maps
- Sensitive information is not accesiable for anonymous (not logged in) users. - Sensitive information is not accesiable for anonymous (not logged in) users.
- User self registration using a voucher system, only people you invite can join your instance. - User self registration using a voucher system, only people you invite can join your instance.
- Collaboration, every user can add informations like tags, photos and external links to your place. - Collaboration, every user can add informations like tags, photos and external links to your place.

View File

@@ -1,199 +0,0 @@
lostplaces-backend
==================
lostplaces-backend is a django (3.x) based webproject. It once wants to
become a software which allows a group of urban explorers to manage,
document and share the locations of lost places while not exposing too
much / any information to the public.
The software is currently in early development status, neither scope,
datalmodel(s) nor features are finalized yet. Therefore we would not
recommend to download or install this piece of software anywhere -
except your local django dev server.
Features
--------
- manage lost places with lots of usefull information
- sensitive information is not accesiable for anonymous (not logged in)
users
- user self registration using a voucher system, only people you invite
can join your instance
- collaboration, every user can add informations like tags, photos and
external links to your place
Dependencies
------------
Right now it depends on the following non-core Python 3 libraries. These
can be installed using the package manager of your distribution or into
the venv locally.
- `django <https://www.djangoproject.com/>`__ django is a high-level
Python Web framework that encourages rapid development and clean,
pragmatic design.
- `easy-thumbnails <https://github.com/SmileyChris/easy-thumbnails>`__
A powerful, yet easy to implement thumbnailing application for Django
1.11+
- `image <https://github.com/francescortiz/image>`__ Image cropping for
django
- `django-widget-tweaks <https://github.com/jazzband/django-widget-tweaks>`__
Tweak the form field rendering in templates, not in python-level form
definitions.
- `django-taggit <https://github.com/jazzband/django-taggit>`__ A
simpler approach to tagging with Django.
Installing a development instance
=================================
Clone the repository
--------------------
``git clone https://git.mowoe.com/reverend/lostplaces-backend.git`` ##
Setting up a (pipenv) virtual environment for development
After having obtained the repository contents (either via .zip download
or git clone), you can easily setup a
`pipenv <https://docs.pipenv.org/>`__ virtual environment. The repo
provides a Pipfile for easy dependency management that does not mess
with your system.
.. code:: shell
$ cd lostplaces-backend
$ pipenv install
$ pipenv shell
(lostplaces-backend) $ lostplaces/manage.py makemigrations
(lostplaces-backend) $ lostplaces/manage.py migrate
(lostplaces-backend) $ lostplaces/manage.py createsuperuser
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
Returning to the venv
---------------------
.. code:: shell
$ cd lostplaces-backend
$ pipenv shell
(lostplaces-backend) $ pipenv update # If dependencies changed, or updates available
(lostplaces-backend) $ lostplaces/manage.py makemigrations # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py migrate # If datamodels changed
(lostplaces-backend) $ lostplaces/manage.py runserver --ipv6
Visit: `admin <http://localhost:8000/admin>`__ for administrative
backend or `frontend <http://localhost:8000/>`__
Installing a productive instance
================================
Currently there are two ways to deploy the lostplaces project: 1.
Cloning this repository, including the configured django instance 2.
Install the package and setup the django instance your self
Cloning the repository
----------------------
Essently, this is the same as installing an development instance, but
without the development server (manage.py runserver) and something
powerfull (Apache, NGINX) instead. You have to configure the webserve to
work with the \*SGI Api respectivly, reference `django's guide for
deployment <https://docs.djangoproject.com/en/3.1/howto/deployment/>`__
for further information.
You also should setup an dedicated database server, the build in SQLite
file is not recommened for production use. Reference `django's guide for
databases <https://docs.djangoproject.com/en/3.1/ref/databases/>`__ for
further information.
Before making the django instance public, you should tweak the config
``settings.py``: 1. Change the secret key, the one found in the config
is already public. Choose something secure (i.e.
`this <https://duckduckgo.com/?q=password+generator+64>`__) 2. Turn off
debug mode by setting ``DEBUG = False`` 3. Tune the localization
settings, see `django's
documentation <https://docs.djangoproject.com/en/3.1/topics/i18n/>`__
Run ``lostplaces/managy.py collectstatic`` you should be ready to go.
Installing the lostplaces\_app to an existing django instance
-------------------------------------------------------------
Installing django and the lostplaces app
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you haven't already setup an django instance, see `django's
documentation <https://docs.djangoproject.com/en/3.1/topics/install/>`__.
After that, download the desired release (probably the latest one) `from
the realeases
page <https://git.mowoe.com/reverend/lostplaces-backend/releases>`__ and
install it using ``pip install --user name-of-the-file.tar.gz``.
*Note: You can run pip install without the --user flag, which will
require root privileges and introduces potential security issues*
Configuring the django instance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now configure your ``settings.py`` as follows: 1. Add the following apps
to the django project
.. code:: python
INSTALLED_APPS = [
...
'lostplaces_app',
'easy_thumbnails',
'widget_tweaks',
'django_taggit'
]
2. Set the URL's and Root-directories for file handling, for example
.. code:: python
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
3. Set the user model (this will be changed in the next release)
.. code:: python
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
4. Set the URL's for login, for example
.. code:: python
LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')
LOGOUT_REDIRECT_URL = reverse_lazy('lostplaces_home')
Configuring the URL's
~~~~~~~~~~~~~~~~~~~~~
In the ``urls.py`` configure the ``urlpatter`` like this
.. code:: python
urlpatterns = [
path('admin/', admin.site.urls),
path('signup/', SignUpView.as_view(), name='signup'), # If you want to use lostplaces sign up view
path('explorers/', include('django.contrib.auth.urls')), # You can change the 'explorers/' to whatever you desire
path('', include('lostplaces_app.urls')), # In this configuration lostplaces will be at the top level of you website, change '' to 'lostplaces/', if you don't want this
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # So django can deliver user uploaded files
Before making the django instance public, you should tweak the config
``settings.py``: 1. Change the secret key, the one found in the config
is already public. Choose something secure (i.e.
`this <https://duckduckgo.com/?q=password+generator+64>`__) 2. Turn off
debug mode by setting ``DEBUG = False`` 3. Tune the localization
settings, see `django's
documentation <https://docs.djangoproject.com/en/3.1/topics/i18n/>`__
Run ``lostplaces/managy.py collectstatic`` you should be ready to go.
Happy developing ;-)

View File

@@ -134,9 +134,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
MEDIA_URL = '/uploads/' MEDIA_URL = '/uploads/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# Use custom user model
AUTH_USER_MODEL = 'lostplaces_app.Explorer'
# Templates to use for authentication # Templates to use for authentication
LOGIN_URL = reverse_lazy('login') LOGIN_URL = reverse_lazy('login')
LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home') LOGIN_REDIRECT_URL = reverse_lazy('lostplaces_home')

View File

@@ -12,17 +12,11 @@ from .forms import ExplorerCreationForm, ExplorerChangeForm
# Register your models here. # Register your models here.
class ExplorerAdmin(UserAdmin):
add_form = ExplorerCreationForm
form = ExplorerChangeForm
model = Explorer
list_display = ['email', 'username',]
class VoucherAdmin(admin.ModelAdmin): class VoucherAdmin(admin.ModelAdmin):
fields = ['code', 'expires', 'created'] fields = ['code', 'expires', 'created']
readonly_fields = ['created'] readonly_fields = ['created']
admin.site.register(Explorer, ExplorerAdmin) admin.site.register(Explorer)
admin.site.register(Voucher, VoucherAdmin) admin.site.register(Voucher, VoucherAdmin)
admin.site.register(Place) admin.site.register(Place)
admin.site.register(PlaceImage) admin.site.register(PlaceImage)

View File

@@ -5,11 +5,12 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import Explorer, Place, PlaceImage, Voucher from django.contrib.auth.models import User
from lostplaces_app.models import Place, PlaceImage, Voucher
class ExplorerCreationForm(UserCreationForm): class ExplorerCreationForm(UserCreationForm):
class Meta: class Meta:
model = Explorer model = User
fields = ('username', 'email') fields = ('username', 'email')
voucher = forms.CharField( voucher = forms.CharField(
max_length=30, max_length=30,
@@ -30,7 +31,7 @@ class ExplorerCreationForm(UserCreationForm):
class ExplorerChangeForm(UserChangeForm): class ExplorerChangeForm(UserChangeForm):
class Meta: class Meta:
model = Explorer model = User
fields = ('username', 'email') fields = ('username', 'email')
class PlaceForm(forms.ModelForm): class PlaceForm(forms.ModelForm):

View File

@@ -10,22 +10,40 @@ import os
import uuid import uuid
from django.db import models from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import AbstractUser
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from easy_thumbnails.fields import ThumbnailerImageField from easy_thumbnails.fields import ThumbnailerImageField
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
# Create your models here. # Create your models here.
class Explorer(AbstractUser):
class Explorer(models.Model):
""" """
Custom user model Profile that is linked to the a User.
Addtional fields wbd Every user has a profile.
""" """
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='explorer'
)
def __str__(self): def __str__(self):
return self.username return self.user.name
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Explorer.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.explorer.save()
class Voucher(models.Model): class Voucher(models.Model):
""" """
@@ -42,6 +60,7 @@ class Voucher(models.Model):
def __str__(self): def __str__(self):
return "Voucher " + str(self.pk) return "Voucher " + str(self.pk)
class Place (models.Model): class Place (models.Model):
""" """
Place defines a lost place (location, name, description etc.). Place defines a lost place (location, name, description etc.).
@@ -91,6 +110,7 @@ class Place (models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def generate_image_upload_path(instance, filename): def generate_image_upload_path(instance, filename):
""" """
Callback for generating path for uploaded images. Callback for generating path for uploaded images.
@@ -98,6 +118,7 @@ def generate_image_upload_path(instance, filename):
return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1] return 'places/' + str(uuid.uuid4())+'.'+filename.split('.')[-1]
class PlaceImage (models.Model): class PlaceImage (models.Model):
""" """
PlaceImage defines an image file object that points to a file in uploads/. PlaceImage defines an image file object that points to a file in uploads/.
@@ -130,6 +151,8 @@ class PlaceImage (models.Model):
return ' '.join([self.place.name, str(self.pk)]) return ' '.join([self.place.name, str(self.pk)])
# These two auto-delete files from filesystem when they are unneeded: # These two auto-delete files from filesystem when they are unneeded:
@receiver(models.signals.post_delete, sender=PlaceImage) @receiver(models.signals.post_delete, sender=PlaceImage)
def auto_delete_file_on_delete(sender, instance, **kwargs): def auto_delete_file_on_delete(sender, instance, **kwargs):
""" """
@@ -140,6 +163,7 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
if os.path.isfile(instance.filename.path): if os.path.isfile(instance.filename.path):
os.remove(instance.filename.path) os.remove(instance.filename.path)
@receiver(models.signals.pre_save, sender=PlaceImage) @receiver(models.signals.pre_save, sender=PlaceImage)
def auto_delete_file_on_change(sender, instance, **kwargs): def auto_delete_file_on_change(sender, instance, **kwargs):
""" """
@@ -160,6 +184,7 @@ def auto_delete_file_on_change(sender, instance, **kwargs):
if os.path.isfile(old_file.path): if os.path.isfile(old_file.path):
os.remove(old_file.path) os.remove(old_file.path)
class ExternalLink(models.Model): class ExternalLink(models.Model):
url = models.URLField(max_length=200) url = models.URLField(max_length=200)
label = models.CharField(max_length=100) label = models.CharField(max_length=100)
@@ -172,6 +197,7 @@ class ExternalLink(models.Model):
) )
submitted_when = models.DateTimeField(auto_now_add=True, null=True) submitted_when = models.DateTimeField(auto_now_add=True, null=True)
class PhotoAlbum(ExternalLink): class PhotoAlbum(ExternalLink):
place = models.ForeignKey( place = models.ForeignKey(
Place, Place,

View File

@@ -6,7 +6,7 @@
<a href="#" class="LP-Link"> <a href="#" class="LP-Link">
<span class="LP-Link__Text">{{tag}}</span> <span class="LP-Link__Text">{{tag}}</span>
</a> </a>
{% if request.user and request.user == config.tagged_item.submitted_by %} {% if request.user and request.user.explorer == config.tagged_item.submitted_by %}
<a href="{% url config.delete_url_name tagged_id=config.tagged_item.id tag_id=tag.id %}" class="LP-Link"> <a href="{% url config.delete_url_name tagged_id=config.tagged_item.id tag_id=tag.id %}" class="LP-Link">
<span class="LP-Tag__Remove RV-Iconized__Container RV-Iconized__Container--extraSmall"> <span class="LP-Tag__Remove RV-Iconized__Container RV-Iconized__Container--extraSmall">
<svg class="RV-Iconized__Icon" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"> <svg class="RV-Iconized__Icon" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
@@ -46,10 +46,14 @@
submit_form.onsubmit = () => false submit_form.onsubmit = () => false
const tagify = new Tagify(input, { const tagify = new Tagify(input, {
'whitelist': [ 'whitelist': [{
{% for tag in all_tags %} %
for tag in all_tags %
}
'{{tag}}', '{{tag}}',
{% endfor %} {
% endfor %
}
] ]
}) })

View File

@@ -62,7 +62,7 @@
<a target="_blank" href="{{photo_album.url}}" class="LP-Link"> <a target="_blank" href="{{photo_album.url}}" class="LP-Link">
<span class="LP-Text">{{photo_album.label}}</span> <span class="LP-Text">{{photo_album.label}}</span>
</a> </a>
{% if user == photo_album.submitted_by or user == place.submitted_by %} {% if user.explorer == photo_album.submitted_by or user.explorer == place.submitted_by %}
<a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album"> <a href="{% url 'photo_album_delete' pk=photo_album.pk%}" class="LP-Link LP-LinkList__ItemHover" title="Delete Photo Album">
<div class="RV-Iconized__Container RV-Iconized__Container--small"> <div class="RV-Iconized__Container RV-Iconized__Container--small">
{% icon 'trash' className="RV-Iconized__Icon" %} {% icon 'trash' className="RV-Iconized__Icon" %}

View File

@@ -1,11 +1,11 @@
from django.db import models as django_models from django.db import models as django_models
from lostplaces_app.models import Explorer from django.contrib.auth.models import User
def mock_user(): def mock_user():
explorer_list = Explorer.objects.all() explorer_list = User.objects.all()
if len(explorer_list) <= 0: if len(explorer_list) <= 0:
return Explorer.objects.create_user( return User.objects.create_user(
username='testpeter', username='testpeter',
password='Develop123' password='Develop123'
) )

View File

@@ -19,7 +19,7 @@ def mock_place_image():
filename=mock.MagicMock(spec=File, name='FileMock'), filename=mock.MagicMock(spec=File, name='FileMock'),
place=mock_place(), place=mock_place(),
submitted_when=datetime.datetime.now(), submitted_when=datetime.datetime.now(),
submitted_by=mock_user() submitted_by=mock_user().explorer
) )
class TestPlaceImage(TestSubmittable, TestCase): class TestPlaceImage(TestSubmittable, TestCase):

View File

@@ -14,7 +14,7 @@ def mock_place():
place = Place.objects.create( place = Place.objects.create(
name='Im a place', name='Im a place',
submitted_when=datetime.datetime.now(), submitted_when=datetime.datetime.now(),
submitted_by=mock_user(), submitted_by=mock_user().explorer,
location='Testtown', location='Testtown',
latitude=50.5, latitude=50.5,
longitude=7.0, longitude=7.0,

View File

@@ -3,7 +3,7 @@ from django.urls import reverse_lazy
from lostplaces_app.models import Place from lostplaces_app.models import Place
from lostplaces_app.models import Explorer from django.contrib.auth.models import User
from lostplaces_app.tests.models.test_place_model import mock_place from lostplaces_app.tests.models.test_place_model import mock_place
from lostplaces_app.tests import mock_user from lostplaces_app.tests import mock_user
@@ -23,7 +23,7 @@ class TestIsAuthenticated(TestCase):
response = self.client.get(url, follow=True) response = self.client.get(url, follow=True)
self.assertRedirects( self.assertRedirects(
response=response, response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']), expected_url='?'.join([str(reverse_lazy('login')), 'next=/place/1/']),
status_code=302, status_code=302,
target_status_code=200, target_status_code=200,
msg_prefix='''Accesing an IsAuthenticated view while not logged should msg_prefix='''Accesing an IsAuthenticated view while not logged should
@@ -47,7 +47,7 @@ class TestIsPlaceSubmitter(TestCase):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_is_no_submitter(self): def test_is_no_submitter(self):
Explorer.objects.create_user( User.objects.create_user(
username='manfred', username='manfred',
password='Develop123' password='Develop123'
) )

View File

@@ -23,7 +23,7 @@ class TestPlaceCreateView(TestCase):
response = self.client.get(url) response = self.client.get(url)
self.assertRedirects( self.assertRedirects(
response=response, response=response,
expected_url='?'.join([str(reverse_lazy('login')), 'redirect_to=/place/1/']), expected_url='?'.join([str(reverse_lazy('login')), 'next=/place/1/']),
status_code=302, status_code=302,
target_status_code=200, target_status_code=200,
msg_prefix='''Accesing PlaceDetailView while not logged should msg_prefix='''Accesing PlaceDetailView while not logged should

View File

@@ -48,7 +48,7 @@ class IsPlaceSubmitter(UserPassesTestMixin, View):
# Check if currently logged in user was the submitter # Check if currently logged in user was the submitter
place_obj = self.get_place() place_obj = self.get_place()
if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user == place_obj.submitted_by: if place_obj and hasattr(place_obj, 'submitted_by') and self.request.user.explorer == place_obj.submitted_by:
return True return True
if self.place_submitter_error_message: if self.place_submitter_error_message:
@@ -69,7 +69,7 @@ class PlaceAssetCreateView(IsAuthenticated, SuccessMessageMixin, CreateView):
self.place = Place.objects.get(pk=place_id) self.place = Place.objects.get(pk=place_id)
response = super().post(request, *args, **kwargs) response = super().post(request, *args, **kwargs)
self.object.place = self.place self.object.place = self.place
self.object.submitted_by = request.user self.object.submitted_by = request.user.explorer
self.object.save() self.object.save()
return response return response
@@ -95,7 +95,7 @@ class PlaceAssetDeleteView(IsAuthenticated, IsPlaceSubmitter, SingleObjectMixin,
if can_edit_place: if can_edit_place:
return True return True
if self.get_object().submitted_by == self.request.user: if self.get_object().submitted_by == self.request.user.explorer:
return True return True
messages.error(self.request, self.permission_denied_message) messages.error(self.request, self.permission_denied_message)

View File

@@ -72,7 +72,7 @@ class PlaceCreateView(IsAuthenticated, View):
place_form = PlaceForm(request.POST) place_form = PlaceForm(request.POST)
if place_form.is_valid(): if place_form.is_valid():
submitter = request.user submitter = request.user.explorer
place = place_form.save(commit=False) place = place_form.save(commit=False)
# Save logged in user as "submitted_by" # Save logged in user as "submitted_by"
place.submitted_by = submitter place.submitted_by = submitter

View File

@@ -1,20 +1,25 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
with open('Readme.md') as f:
readme = f.read()
setup( setup(
name='django-lostplaces', name='django-lostplaces',
version='0.1.a2', version='0.1.a5',
description='A django app to manage lost places', description='A django app to manage lost places',
author='Reverend :: Commander1024', author='Reverend',
author_email='reverend@reverend2048.de :: commander@commander1024.de', author_email='reverend@reverend2048.de',
url='https://git.mowoe.com/reverend/lostplaces-backend', url='https://git.mowoe.com/reverend/lostplaces-backend',
packages=find_packages(exclude=['lostplaces']), packages=find_packages(exclude=['lostplaces']),
long_description=readme,
long_description_content_type='text/markdown',
classifiers=[ classifiers=[
'Development Status :: 3 - Alpha', 'Development Status :: 3 - Alpha',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Intended Audience :: Explorer :: Photographers :: Proper People',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python :: 3.7' 'Programming Language :: Python :: 3.7',
'Framework :: Django'
], ],
install_requires=[ install_requires=[
'django', 'django',