Skip to content

Commit efd0d0f

Browse files
author
Marcel
authored
Merge pull request #17 from Segelzwerg/develop-39python
develop/prepare-39
2 parents f8f7567 + ad684c6 commit efd0d0f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+363
-302
lines changed

.github/workflows/python-check.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
strategy:
1111
matrix:
1212
os: [ubuntu-18.04, ubuntu-20.04]
13-
python-version: [3.8]
13+
python-version: [3.9]
1414
fail-fast: false
1515
max-parallel: 4
1616
runs-on: ${{matrix.os}}
@@ -32,14 +32,14 @@ jobs:
3232
run: |
3333
python --version
3434
python3 --version
35+
sudo apt-get update
3536
sudo apt-get install ffmpeg
36-
python3 -m pip install --upgrade pip setuptools
3737
python3 -m pip install -r requirements.txt
3838
3939
- name: Python Pylint Github Action
4040
run: |
4141
python3 -m pylint --version
42-
python3 -m pylint --load-plugins pylint_flask_sqlalchemy --rcfile=.pylintrc -j 2 family_foto tests
42+
python3 -m pylint --load-plugins=pylint_flask,pylint_flask_sqlalchemy --rcfile=.pylintrc family_foto tests
4343
4444
- name: Python Pytest with Coverage
4545
run: |

.gitignore

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
# Project exclude paths
2-
/venv/
3-
/.idea/
4-
/app.db
5-
/coverage.xml
6-
/.coverage
1+
venv*/
2+
.idea/
3+
app.db
4+
coverage.xml
5+
.coverage
6+
7+
*.pyc
8+
__pycache__/
9+
10+
instance/
11+
12+
.pytest_cache/
13+
htmlcov/
14+
15+
dist/
16+
build/
17+
*.egg-info/

family_foto/__init__.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import os
2+
from typing import Any
3+
4+
import flask_uploads
5+
from flask import Flask
6+
from flask_debugtoolbar import DebugToolbarExtension
7+
from flask_login import LoginManager
8+
9+
from family_foto.const import UPLOADED_PHOTOS_DEST_RELATIVE, UPLOADED_VIDEOS_DEST_RELATIVE, \
10+
RESIZED_DEST_RELATIVE, RESIZED_DEST
11+
from family_foto.logger import log
12+
from family_foto.models import db
13+
from family_foto.models.user import User
14+
from family_foto.models.user_settings import UserSettings
15+
16+
login_manager = LoginManager()
17+
18+
19+
# pylint: disable=import-outside-toplevel
20+
def create_app(test_config: dict[str, Any] = None, test_instance_path: str = None) -> Flask:
21+
"""
22+
Create the Flask application.
23+
:param test_config: config override
24+
:param test_instance_path: instance path override
25+
:return: the configured app
26+
"""
27+
# create and configure the app
28+
app = Flask(__name__, instance_relative_config=True, instance_path=test_instance_path)
29+
app.config.from_mapping(
30+
SECRET_KEY=os.environ.get('SECRET_KEY') or 'very-secret-key',
31+
DATABASE_URL_TEMPLATE='sqlite:///{instance_path}/app.db',
32+
SQLALCHEMY_TRACK_MODIFICATIONS=False,
33+
UPLOADED_PHOTOS_DEST_RELATIVE='photos',
34+
UPLOADED_VIDEOS_DEST_RELATIVE='videos',
35+
RESIZED_DEST_RELATIVE='resized-images'
36+
)
37+
38+
if test_config is None:
39+
# load the instance config, if it exists, when not testing
40+
app.config.from_pyfile('config.py', silent=True)
41+
else:
42+
# load the test config if passed in
43+
app.config.from_mapping(test_config)
44+
45+
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL') or app.config[
46+
'DATABASE_URL_TEMPLATE'].format(instance_path=app.instance_path)
47+
app.config['UPLOADED_PHOTOS_DEST'] = os.path.join(app.instance_path,
48+
app.config[UPLOADED_PHOTOS_DEST_RELATIVE])
49+
app.config['UPLOADED_VIDEOS_DEST'] = os.path.join(app.instance_path,
50+
app.config[UPLOADED_VIDEOS_DEST_RELATIVE])
51+
app.config[RESIZED_DEST] = os.path.join(app.instance_path,
52+
app.config[RESIZED_DEST_RELATIVE])
53+
54+
# ensure the instance folder exists
55+
try:
56+
os.makedirs(app.instance_path)
57+
except OSError:
58+
pass
59+
60+
_ = DebugToolbarExtension(app)
61+
62+
from family_foto.api import api_bp
63+
app.register_blueprint(api_bp)
64+
65+
from family_foto.web import web_bp
66+
app.register_blueprint(web_bp)
67+
68+
db.init_app(app)
69+
db.app = app
70+
db.create_all()
71+
72+
login_manager.init_app(app)
73+
74+
from family_foto.web import photos, videos
75+
flask_uploads.configure_uploads(app, (photos, videos))
76+
77+
add_user('admin', 'admin')
78+
79+
return app
80+
81+
82+
def add_user(username: str, password: str) -> User:
83+
"""
84+
This registers an user.
85+
:param username: name of the user
86+
:param password: plain text password
87+
"""
88+
user = User(username=username)
89+
user.set_password(password)
90+
exists = User.query.filter_by(username=username).first()
91+
if exists:
92+
log.warning(f'{user.username} already exists.')
93+
return exists
94+
95+
user_settings = UserSettings(user_id=user.id)
96+
user.settings = user_settings
97+
98+
db.session.add(user_settings)
99+
db.session.add(user)
100+
db.session.commit()
101+
log.info(f'{user.username} registered.')
102+
return user
103+
104+
105+
@login_manager.user_loader
106+
def load_user(user_id: int):
107+
"""
108+
Loads a user from the database.
109+
:param user_id:
110+
:return: An user if exists.
111+
"""
112+
return User.query.get(int(user_id))

family_foto/api/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
from flask import Blueprint
22

3-
api = Blueprint('api', __name__)
4-
5-
# avoid circular imports
6-
# pylint: disable=wrong-import-position
7-
from family_foto.api import errors, auth
3+
api_bp = Blueprint('api', __name__, url_prefix='/api')

family_foto/api/auth.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
from typing import Union
2-
31
from flask import jsonify
42
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
53
from flask_login import current_user
64

7-
from family_foto.api import api
5+
from family_foto.api import api_bp
86
from family_foto.api.errors import error_response
97
from family_foto.logger import log
108
from family_foto.models import db
@@ -15,7 +13,7 @@
1513
token_auth = HTTPTokenAuth()
1614

1715

18-
@api.route('/token', methods=['POST'])
16+
@api_bp.route('/token', methods=['POST'])
1917
@basic_auth.login_required
2018
def get_token():
2119
"""
@@ -29,7 +27,7 @@ def get_token():
2927

3028

3129
@basic_auth.verify_password
32-
def verify_password(username: str, password: str) -> Union[None, User]:
30+
def verify_password(username: str, password: str) -> [None, User]:
3331
"""
3432
Verifies the password for given user.
3533
:param username: the username string

family_foto/api/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def error_response(status_code: int, message: str = None):
88
:param status_code: http error code
99
:param message: optional message
1010
"""
11-
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unkown error')}
11+
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
1212
if message:
1313
payload['message'] = message
1414
response = jsonify(payload)

family_foto/config.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

family_foto/const.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
UPLOADED_PHOTOS_DEST = 'UPLOADED_PHOTOS_DEST'
2+
UPLOADED_PHOTOS_DEST_RELATIVE = 'UPLOADED_PHOTOS_DEST_RELATIVE'
3+
4+
UPLOADED_VIDEOS_DEST = 'UPLOADED_VIDEOS_DEST'
5+
UPLOADED_VIDEOS_DEST_RELATIVE = 'UPLOADED_VIDEOS_DEST_RELATIVE'
6+
7+
RESIZED_DEST = 'RESIZED_DEST'
8+
RESIZED_DEST_RELATIVE = 'RESIZED_DEST_RELATIVE'
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from wtforms import SelectMultipleField, widgets
1+
from wtforms import SelectMultipleField
2+
from wtforms.widgets import ListWidget, CheckboxInput
23

34

45
class MultiCheckboxField(SelectMultipleField):
56
"""
67
Field for selecting multiple fields from checkbox list.
78
"""
8-
widget = widgets.ListWidget(html_tag='ul', prefix_label=False)
9-
option_widget = widgets.CheckboxInput()
9+
widget = ListWidget(html_tag='ul', prefix_label=False)
10+
option_widget = CheckboxInput()

family_foto/forms/upload_form.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ class UploadForm(FlaskForm):
77
"""
88
Data form of upload mask.
99
"""
10-
file = MultipleFileField('File',
11-
validators=[FileRequired(),
12-
FileAllowed(['jpg', 'png'],
13-
'Images Only!')])
10+
file = MultipleFileField('File', validators=[FileRequired(),
11+
FileAllowed(['jpg', 'png'], 'Images Only!')])
1412
submit = SubmitField('Upload')

family_foto/models/file.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime
44
from typing import List, Union
55

6+
from flask import current_app
67
from sqlalchemy import ForeignKey
78
from sqlalchemy.orm import relationship
89

@@ -36,21 +37,22 @@ def path(self):
3637
"""
3738
Path of the file on the server.
3839
"""
39-
raise NotImplementedError('Each file type has it\'s directory.')
40+
raise NotImplementedError('Each file type has its directory.')
4041

4142
@property
4243
def abs_path(self):
4344
"""
4445
Returns absolute path of the photo.
4546
"""
46-
return os.path.abspath(f'/photo/{self.filename}')
47+
return os.path.abspath(os.path.join(current_app.instance_path, self.path))
4748

4849
@property
50+
@abstractmethod
4951
def image_view(self):
5052
"""
5153
Returns path to image viewer template of this photo.
5254
"""
53-
return f'/image/{self.filename}'
55+
raise NotImplementedError('Each file type has its directory.')
5456

5557
@property
5658
@abstractmethod
@@ -61,18 +63,20 @@ def meta(self):
6163
raise NotImplementedError('Each media type has is custom meta data retriever.')
6264

6365
@property
66+
@abstractmethod
6467
def height(self):
6568
"""
6669
Returns the image height.
6770
"""
68-
return int(self.meta['ExifImageHeight'])
71+
raise NotImplementedError('Each media type has is custom meta data retriever.')
6972

7073
@property
74+
@abstractmethod
7175
def width(self):
7276
"""
7377
Returns image width
7478
"""
75-
return int(self.meta['ExifImageWidth'])
79+
raise NotImplementedError('Each media type has is custom meta data retriever.')
7680

7781
@abstractmethod
7882
def thumbnail(self, width: int, height: int):
@@ -81,7 +85,7 @@ def thumbnail(self, width: int, height: int):
8185
:param width: thumbnail width in pixel
8286
:param height: thumbnail height in pixel (aspect ratio will be kept)
8387
"""
84-
raise NotImplementedError(f'{self} is abstract and not thumbnail() is not implemented.')
88+
raise NotImplementedError(f'{self} is abstract and thumbnail() is not implemented.')
8589

8690
def share_with(self, other_users: Union[User, List[User]]) -> None:
8791
"""

0 commit comments

Comments
 (0)