Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/event upload image #294

Merged
Merged
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
779de2d
feat: add i18n support (#115)
Gonzom Jan 30, 2021
6370ef2
Revert "feat: add i18n support (#115)" (#161)
yammesicka Jan 30, 2021
7f38da9
Update our production site. (#209)
yammesicka Feb 5, 2021
585559a
feat: event image uplaod, initial changes dcommit
imimouni Feb 13, 2021
2de8430
check - find source of error
imimouni Feb 15, 2021
cc5e89a
feat: upload image to event and view it
imimouni Feb 15, 2021
b554a5a
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 15, 2021
e364e34
fix: flake8 after merge
imimouni Feb 15, 2021
3cd5f53
fix: pytest fix for git
imimouni Feb 15, 2021
9100b61
fix: spaces new imga flake8
imimouni Feb 15, 2021
44e3959
fix: test_event file path to image
imimouni Feb 15, 2021
058e4fa
fix: escape path flake8
imimouni Feb 15, 2021
35dd94d
fix: file path to delete test_event
imimouni Feb 15, 2021
5e84f4b
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 15, 2021
5fadcd2
fix: remove metadata from images in event.py process_image
imimouni Feb 16, 2021
64bb94e
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 16, 2021
3f4a6eb
fix: flake8 after merge
imimouni Feb 16, 2021
26ab391
fix: flake8 after merge
imimouni Feb 16, 2021
2c48e0b
Merge branch 'main' into develop
yammesicka Feb 18, 2021
5a69ad1
Apply automatic translatable string changes
yammesicka Feb 18, 2021
253fbfe
fix: Profile page head problems (#321)
yammesicka Feb 18, 2021
223d9eb
Apply automatic translatable string changes
yammesicka Feb 18, 2021
24f1238
Feature/login (#293) (#322)
yammesicka Feb 18, 2021
23a3370
Apply automatic translatable string changes
yammesicka Feb 18, 2021
4a564a4
merge to develop
imimouni Feb 20, 2021
2cf277b
merge to develop - delete ectra files
imimouni Feb 20, 2021
8d57810
Delete LICENSE.md
imimouni Feb 20, 2021
b60f072
readd deleted correct license
imimouni Feb 20, 2021
a0bc8b5
check image file
imimouni Feb 20, 2021
5c4a4cf
verify image file
imimouni Feb 20, 2021
f33b495
Merge branch 'main' of https://github.com/PythonFreeCourse/calendar i…
imimouni Feb 20, 2021
b5f9250
merge to develop + conflicts
imimouni Feb 20, 2021
b259334
fix: test_eventedit_post_wrong in test_event
imimouni Feb 20, 2021
0ecef90
fix: make sure app/locales matches develop after merge
imimouni Feb 20, 2021
cd7823e
fix: make sure app/locales matches develop after merge - develop!
imimouni Feb 20, 2021
8a8a3ff
fix: vc_link
imimouni Feb 20, 2021
06c2c5e
sort imports
imimouni Feb 21, 2021
8623667
fix: event_image_directory - config+ references
imimouni Feb 21, 2021
e148c2f
fix: test_event event image path
imimouni Feb 21, 2021
1e7d167
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 22, 2021
33571af
fix: app.py event images mount
imimouni Feb 22, 2021
6b428a2
Merge branch 'feature/event_upload_image' of https://github.com/imimo…
imimouni Feb 22, 2021
a99cc30
fix: schema.md
imimouni Feb 22, 2021
a91a57a
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 22, 2021
f31a796
try to add file to event_images
imimouni Feb 22, 2021
4a27e52
Merge branch 'feature/event_upload_image' of https://github.com/imimo…
imimouni Feb 22, 2021
7218c80
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 22, 2021
a19d484
flake8 after web merge
imimouni Feb 22, 2021
baef690
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 24, 2021
5463b9b
fix: event image directory path and name
imimouni Feb 24, 2021
5a12423
merge to develop
imimouni Feb 24, 2021
5cd61fa
fix: upload path
imimouni Feb 24, 2021
0421816
fix: upload path v2
imimouni Feb 24, 2021
c80e241
merge to develop pull isort
imimouni Feb 24, 2021
db9e200
fix flake8 after merge
imimouni Feb 24, 2021
3bf1200
fix comment in config
imimouni Feb 24, 2021
800a8a9
Merge branch 'develop' into feature/event_upload_image
imimouni Feb 24, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/config.py.example
Expand Up @@ -27,6 +27,10 @@ PSQL_ENVIRONMENT = False
MEDIA_DIRECTORY = 'media'
PICTURE_EXTENSION = '.png'
AVATAR_SIZE = (120, 120)
"""For security reasons, set the upload path to a local absolute path.
Or for testing environment - just specify a folder name
that will be created under /app/"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer # before each line - docstring are reserved for general documentation

UPLOAD_DIRECTORY = 'event_images'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yammesicka this is the comment in the config example to explain to users it's preferable to use a local directory.



# DEFAULT WEBSITE LANGUAGE
Expand Down
2 changes: 2 additions & 0 deletions app/database/models.py
Expand Up @@ -103,6 +103,7 @@ class Event(Base):
invitees = Column(String)
privacy = Column(String, default=PrivacyKinds.Public.name, nullable=False)
emotion = Column(String, nullable=True)
image = Column(String, nullable=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's actually a String, unless you converted it to some textual format as base64. In any other case, it's probably Binary or something like that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's str as it is just the file name (similar to what was done on upload profile image)

availability = Column(Boolean, default=True, nullable=False)

owner_id = Column(Integer, ForeignKey("users.id"))
Expand Down Expand Up @@ -497,6 +498,7 @@ class InternationalDays(Base):

# insert language data


# Credit to adrihanu https://stackoverflow.com/users/9127249/adrihanu
# https://stackoverflow.com/questions/17461251
def insert_data(target, session: Session, **kw):
Expand Down
26 changes: 19 additions & 7 deletions app/dependencies.py
Expand Up @@ -15,15 +15,27 @@
TEMPLATES_PATH = os.path.join(APP_PATH, "templates")
SOUNDS_PATH = os.path.join(STATIC_PATH, "tracks")
templates = Jinja2Templates(directory=TEMPLATES_PATH)
templates.env.add_extension('jinja2.ext.i18n')
templates.env.add_extension("jinja2.ext.i18n")

# Configure logger
logger = LoggerCustomizer.make_logger(config.LOG_PATH,
config.LOG_FILENAME,
config.LOG_LEVEL,
config.LOG_ROTATION_INTERVAL,
config.LOG_RETENTION_INTERVAL,
config.LOG_FORMAT)
logger = LoggerCustomizer.make_logger(
config.LOG_PATH,
config.LOG_FILENAME,
config.LOG_LEVEL,
config.LOG_ROTATION_INTERVAL,
config.LOG_RETENTION_INTERVAL,
config.LOG_FORMAT,
)

if os.path.isdir(config.UPLOAD_DIRECTORY):
UPLOAD_PATH = config.UPLOAD_DIRECTORY
else:
try:
UPLOAD_PATH = os.path.join(os.getcwd(), config.UPLOAD_DIRECTORY)
os.mkdir(UPLOAD_PATH)
except OSError as e:
logger.critical(e)
raise OSError(e)
yammesicka marked this conversation as resolved.
Show resolved Hide resolved


def get_db() -> Session:
Expand Down
1 change: 0 additions & 1 deletion app/locales/en/LC_MESSAGES/base.po
Expand Up @@ -130,4 +130,3 @@ msgstr ""

#~ msgid "Agenda"
#~ msgstr ""

1 change: 0 additions & 1 deletion app/locales/he/LC_MESSAGES/base.po
Expand Up @@ -130,4 +130,3 @@ msgstr "בדיקת תרגום בפייתון"

#~ msgid "Agenda"
#~ msgstr ""

6 changes: 6 additions & 0 deletions app/main.py
Expand Up @@ -12,6 +12,7 @@
MEDIA_PATH,
SOUNDS_PATH,
STATIC_PATH,
UPLOAD_PATH,
get_db,
logger,
templates,
Expand Down Expand Up @@ -39,6 +40,11 @@ def create_tables(engine, psql_environment):
app = FastAPI(title="Pylander", docs_url=None)
app.mount("/static", StaticFiles(directory=STATIC_PATH), name="static")
app.mount("/media", StaticFiles(directory=MEDIA_PATH), name="media")
app.mount(
"/event_images",
StaticFiles(directory=UPLOAD_PATH),
name="event_images",
)
app.mount("/static/tracks", StaticFiles(directory=SOUNDS_PATH), name="sounds")
app.logger = logger

Expand Down
41 changes: 39 additions & 2 deletions app/routers/event.py
@@ -1,10 +1,12 @@
import io
import json
import urllib
from datetime import datetime as dt
from operator import attrgetter
from typing import Any, Dict, List, Optional, Tuple

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, File, HTTPException, Request
from PIL import Image
from pydantic import BaseModel
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
Expand All @@ -14,8 +16,9 @@
from starlette.responses import RedirectResponse, Response
from starlette.templating import _TemplateResponse

from app.config import PICTURE_EXTENSION
from app.database.models import Comment, Event, User, UserEvent
from app.dependencies import get_db, logger, templates
from app.dependencies import UPLOAD_PATH, get_db, logger, templates
from app.internal import comment as cmt
from app.internal.emotion import get_emotion
from app.internal.event import (
Expand All @@ -29,6 +32,7 @@
from app.internal.utils import create_model, get_current_user
from app.routers.categories import get_user_categories

IMAGE_HEIGHT = 200
EVENT_DATA = Tuple[Event, List[Dict[str, str]], str]
TIME_FORMAT = "%Y-%m-%d %H:%M"
START_FORMAT = "%A, %d/%m/%Y %H:%M"
Expand Down Expand Up @@ -105,6 +109,7 @@ async def eventedit(
@router.post("/edit", include_in_schema=False)
async def create_new_event(
request: Request,
event_img: bytes = File(None),
session=Depends(get_db),
) -> Response:
data = await request.form()
Expand Down Expand Up @@ -164,6 +169,11 @@ async def create_new_event(
privacy=privacy,
)

if event_img:
image = process_image(event_img, event.id)
event.image = image
session.commit()

messages = get_messages(session, event, uninvited_contacts)
return RedirectResponse(
router.url_path_for("eventview", event_id=event.id)
Expand All @@ -172,6 +182,31 @@ async def create_new_event(
)


def process_image(
img: bytes,
event_id: int,
img_height: int = IMAGE_HEIGHT,
) -> str:
"""Resized and saves picture without exif (to avoid malicious date))
according to required height and keep aspect ratio"""
try:
image = Image.open(io.BytesIO(img))
except IOError:
error_message = "The uploaded file is not a valid image"
logger.exception(error_message)
return
width, height = image.size
height_to_req_height = img_height / float(height)
new_width = int(float(width) * float(height_to_req_height))
resized = image.resize((new_width, img_height), Image.ANTIALIAS)
file_name = f"{event_id}{PICTURE_EXTENSION}"
image_data = list(resized.getdata())
image_without_exif = Image.new(resized.mode, resized.size)
image_without_exif.putdata(image_data)
image_without_exif.save(f"{UPLOAD_PATH}/{file_name}")
return file_name


def get_waze_link(event: Event) -> str:
"""Get a waze navigation link to the event location.

Expand Down Expand Up @@ -430,6 +465,7 @@ def create_event(
availability: bool = True,
is_google_event: bool = False,
privacy: str = PrivacyKinds.Public.name,
image: Optional[str] = None,
):
"""Creates an event and an association."""

Expand All @@ -455,6 +491,7 @@ def create_event(
category_id=category_id,
availability=availability,
is_google_event=is_google_event,
image=image,
)
create_model(db, UserEvent, user_id=owner_id, event_id=event.id)
return event
Expand Down
2 changes: 1 addition & 1 deletion app/templates/eventedit.html
@@ -1,5 +1,5 @@
<link href="{{ url_for('static', path='event/eventedit.css') }}" rel="stylesheet">
<form name="eventeditform" method="POST">
<form name="eventeditform" method="POST" enctype="multipart/form-data">
<!-- Temporary nav layout based on bootstrap -->
<ul class="nav nav-tabs" id="event_edit_nav" role="tablist">
<li class="nav-item">
Expand Down
Expand Up @@ -54,6 +54,12 @@
<input type="text" id="invited" name="invited" placeholder="Invited emails, separated by commas">
</div>

<div class="form_row">
<label for="event_img">Image: </label>
<input type="file" id="event_img" name="event_img"
accept="image/png, image/jpeg">
</div>

<div class="form_row textarea">
<textarea id="say" name="description" placeholder="Description"></textarea>
</div>
Expand All @@ -71,8 +77,8 @@
<div class="form_row_end">
<label for="event_type">All-day:</label>
<select id="event_type" name="event_type" required>
<option value="on">Yes</option>
<option value="off" selected>No</option>
<option value="on">Yes</option>
<option value="off" selected>No</option>
</select>

<label for="availability">Availability:</label>
Expand Down
Expand Up @@ -7,6 +7,9 @@ <h1>{{ event.title }}</h1>
<!-- <span class="icon">PRIVACY</span>-->
</div>
</div>
<div class="event-image-container">
<img class="event-image" src="{{ '' if not event.image else url_for('media', path=event.image) }}">
</div>
<div class="event_info_row">
<span class="icon">ICON</span>
<time datetime="{{ event.start }}">{{ event.start.strftime(start_format )}}</time>
Expand Down
2 changes: 1 addition & 1 deletion schema.md
Expand Up @@ -47,4 +47,4 @@
├── test_categories.py
├── test_email.py
├── test_event.py
└── test_profile.py
└── test_profile.py
35 changes: 34 additions & 1 deletion tests/test_event.py
@@ -1,15 +1,18 @@
import json
import os
from datetime import datetime, timedelta

import pytest
from fastapi import HTTPException, Request
from fastapi.testclient import TestClient
from PIL import Image
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.elements import Null
from starlette import status

from app.config import PICTURE_EXTENSION
from app.database.models import Comment, Event
from app.dependencies import get_db
from app.dependencies import UPLOAD_PATH, get_db
yammesicka marked this conversation as resolved.
Show resolved Hide resolved
from app.internal.privacy import PrivacyKinds
from app.internal.utils import delete_instance
from app.main import app
Expand Down Expand Up @@ -539,6 +542,36 @@ def test_deleting_an_event_does_not_exist(event_test_client, event):
assert response.status_code == status.HTTP_404_NOT_FOUND


def test_event_with_image(event_test_client, client, session):
img = Image.new("RGB", (60, 30), color="red")
img.save("pil_red.png")
yammesicka marked this conversation as resolved.
Show resolved Hide resolved
with open("pil_red.png", "rb") as img:
imgstr = img.read()
files = {"event_img": imgstr}
data = {**CORRECT_EVENT_FORM_DATA}
response = event_test_client.post(
client.app.url_path_for("create_new_event"),
data=data,
files=files,
)
event_created = session.query(Event).order_by(Event.id.desc()).first()
event_id = event_created.id
is_event_image = f"{event_id}{PICTURE_EXTENSION}" == event_created.image
assert response.ok
assert (
client.app.url_path_for("eventview", event_id=event_id).strip(
f"{event_id}",
)
in response.headers["location"]
)
assert is_event_image is True
event_image_path = os.path.join(UPLOAD_PATH, event_created.image)
os.remove(event_image_path)
os.remove("pil_red.png")
session.delete(event_created)
session.commit()


def test_can_show_event_public(event, session, user):
assert event_to_show(event, session) == event
assert event_to_show(event, session, user) == event
Expand Down