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

Add file uploader #36

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f3ce7df
Bump dependencies
raynei86 Jul 22, 2024
90ccb4d
Fix import errors with Flask removing `Markup` and `escape`
raynei86 Jul 22, 2024
ae0836c
Add flask app environment variable
raynei86 Jul 22, 2024
6ba6269
Fix broken imports in auth views
raynei86 Jul 22, 2024
9b0a12b
Fixed inconsistency between env variable and code
raynei86 Jul 22, 2024
9dbd242
Bump pipfile dependencies
raynei86 Jul 22, 2024
5e0ed6b
Migrate from flask-script to Flask builtin CLI commands
raynei86 Jul 22, 2024
4ccd35a
Migrate from flask-elasticsearch to use official python client
raynei86 Jul 22, 2024
dfe1ab7
Add rudimentary support for story image upload
raynei86 Jul 24, 2024
677acd5
Add rudimentary file size validation
raynei86 Jul 24, 2024
44c4ed3
Chunk story image upload
raynei86 Jul 24, 2024
5a03b04
Make story image upload asynchronous
raynei86 Jul 25, 2024
8aeeed5
Check if all chunks of image are uploaded
raynei86 Jul 25, 2024
dc8ff43
Fix chunk calculation errors
raynei86 Jul 25, 2024
33f6a3a
Simplify file upload view function
raynei86 Jul 25, 2024
f0219b4
Add progress bar to image upload
raynei86 Jul 29, 2024
7bd9ce3
Fix missing progress bar
raynei86 Jul 29, 2024
7646c17
Add azure related settings
raynei86 Jul 29, 2024
d00d2b2
Add blob_name column for azure
raynei86 Jul 29, 2024
52cce58
Use azure instead of image host
raynei86 Jul 29, 2024
0a2e236
Add staging phase for image upload
raynei86 Jul 29, 2024
c51e623
Fix bug when user does not share story image
raynei86 Jul 30, 2024
5561162
Refactor file upload view function
raynei86 Jul 30, 2024
e6ee077
Move `current_story_id` and `stories_amount` to app/lib/utils.py
raynei86 Jul 30, 2024
31359ba
Create function to acquire story image
raynei86 Jul 30, 2024
322b003
Display story image on home page
raynei86 Jul 30, 2024
d83cb3e
Update pipfile.lock
raynei86 Jul 30, 2024
80a77cf
Add local setup instructions
raynei86 Jul 30, 2024
9432df1
Add migration for azure blob name
raynei86 Jul 30, 2024
b039693
Document file uploader
raynei86 Jul 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
FLASK_APP=manage.py

# Flask-WTForms
SECRET_KEY=

Expand All @@ -20,4 +22,15 @@ RECAPTCHA_THRESHOLD=
FEATURED_DATA=

# SQLAlchemy Database Configuration
SQLALCHEMY_DATABASE_URI=
DATABASE_URL=

# File upload config
UPLOAD_DIRECTORY=

# Image host
IMAGE_HOST_URL=

# Azure
AZURE_STORAGE_ACCOUNT_NAME=
AZURE_CONTAINER_NAME=
AZURE_STORAGE_ACCOUNT_KEY=
59 changes: 30 additions & 29 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,36 @@ verify_ssl = true
[dev-packages]

[packages]
alembic = "==0.8.10"
click = "==6.7"
dominate = "==2.3.1"
elasticsearch = "==5.2.0"
itsdangerous = "==0.24"
pytz = "==2016.10"
python-dotenv = "==0.6.3"
python-editor = "==1.0.3"
requests = "==2.11.1"
urllib3 = "==1.20"
visitor = "==0.1.3"
Flask = "==0.12"
Flask-Admin = "==1.5.0"
Flask-Bootstrap = "==3.3.7.1"
Flask-Elasticsearch = "==0.2.5"
Flask-Login = "==0.4.0"
Flask-Mail = "==0.9.1"
Flask-Migrate = "==2.0.3"
Flask-Moment = "==0.5.1"
Flask-Script = "==2.0.5"
Flask-SQLAlchemy = "==2.1"
Flask-WTF = "==0.14.2"
Jinja2 = "==2.9.5"
Mako = "==1.0.6"
MarkupSafe = "==0.23"
SQLAlchemy = "==1.1.5"
Werkzeug = "==0.11.15"
WTForms = "==2.1"
alembic = "*"
click = "*"
dominate = "*"
elasticsearch = "*"
itsdangerous = "*"
pytz = "*"
python-dotenv = "*"
python-editor = "*"
requests = "*"
urllib3 = "*"
visitor = "*"
psycopg2 = "*"
flask = "*"
mako = "*"
jinja2 = "*"
markupsafe = "*"
sqlalchemy = "*"
wtforms = "*"
psycopg2-binary = "*"
flask-admin = "*"
flask-bootstrap = "*"
flask-login = "*"
flask-migrate = "*"
flask-moment = "*"
flask-sqlalchemy = "*"
flask-wtf = "*"
werkzeug = "*"
flask-mail = "*"
flask-elasticsearch = "*"
azure-storage-blob = "*"

[requires]
python_version = "3.8"
python_version = "3.12"
726 changes: 632 additions & 94 deletions Pipfile.lock

Large diffs are not rendered by default.

30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Womens Activism #
## Development Environment Setup ##
# Womens Activism
## Development Environment Setup
The following dependencies are required:
- Postgresql
- libpq
- Elasticsearch

## Vagrant
*Make sure your version of VirtualBox matches the version used to create the vagrant box.*

1. Copy `rhel-6.8.virtualbox.box` from the repository into your desired directory.
Expand All @@ -12,4 +18,22 @@
6. Run `vagrant plugin install vagrant-reload vagrant-vbguest`
7. Run `vagrant up`
- If there is an error during this process, try running `vagrant provision`
8. Run `vagrant ssh` to connect to your development environment.
8. Run `vagrant ssh` to connect to your development environment.

## Local
The project uses pipenv to manage dependencies.
Clone the repository and run the following:

```python
pipenv install
```

Then run the following to enter the virtual environment:
```python
pipenv shell
```

Start the flask session like so:
```shell
flask run
```
5 changes: 2 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
from flask_elasticsearch import FlaskElasticsearch
from flask_login import LoginManager
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import CSRFProtect
from config import config
from flask_mail import Mail
from elasticsearch import Elasticsearch

bootstrap = Bootstrap()
csrf = CSRFProtect()
db = SQLAlchemy()
es = FlaskElasticsearch()
moment = Moment()
mail = Mail()

Expand All @@ -28,7 +27,7 @@ def create_app(config_name):
config[config_name].init_app(app)

bootstrap.init_app(app)
es.init_app(app, use_ssl=app.config['ELASTICSEARCH_USE_SSL'])
app.elasticsearch = Elasticsearch(app.config['ELASTICSEARCH_URL'])
db.init_app(app)
csrf.init_app(app)
moment.init_app(app)
Expand Down
3 changes: 2 additions & 1 deletion app/auth/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import render_template, redirect, request, url_for, flash, escape
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from markupsafe import escape

from app import db
from app.auth.utils import create_login_event
Expand Down
3 changes: 2 additions & 1 deletion app/edit/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from app.edit import edit
from flask import render_template, redirect, url_for, flash, request, Markup, abort, escape
from flask import render_template, redirect, url_for, flash, request, abort
from app.models import Tags, Stories, Users
from app.edit.forms import StoryForm
from app.edit.utils import update_story, update_user
from app.lib.utils import create_user
from sqlalchemy.orm.exc import NoResultFound
from flask_login import login_required
from markupsafe import Markup, escape


@edit.route('/<story_id>', methods=['GET', 'POST'])
Expand Down
3 changes: 2 additions & 1 deletion app/export/views.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import csv
from datetime import datetime

from flask import render_template, request, flash, redirect, url_for, Markup
from flask import render_template, request, flash, redirect, url_for
from flask.helpers import send_file
from flask_login import login_required
from io import StringIO, BytesIO
from sqlalchemy import or_
from markupsafe import Markup

from app.export import export
from app.export.forms import ExportForm
Expand Down
49 changes: 49 additions & 0 deletions app/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"""
import uuid
import re
from datetime import datetime, timedelta

from flask import current_app, render_template, url_for
from azure.storage.blob import generate_blob_sas, BlobSasPermissions

from app.constants.event_type import STORY_CREATED, USER_CREATED, NEW_SUBSCRIBER, UNSUBSCRIBED_EMAIL, UNSUBSCRIBED_PHONE
from app.constants.user_type_auth import ANONYMOUS_USER
Expand All @@ -22,6 +24,7 @@ def create_story(activist_first,
tags,
content,
activist_url,
image_blob_name,
image_url,
video_url,
user_guid):
Expand All @@ -36,6 +39,7 @@ def create_story(activist_first,
:param tags: a string array containing the selected tags associated with the activist
:param content: the content of the story
:param activist_url: a url containing additional information about the activist
:param image_blob_name: the name of the image blob on azure
:param image_url: a url containing an image link
:param video_url: a url containing a
:param user_guid: the guid of the user who created the story
Expand All @@ -59,6 +63,7 @@ def create_story(activist_first,
activist_end=activist_end,
content=content,
activist_url=activist_url if activist_url else None,
image_blob_name=image_blob_name if image_blob_name else None,
image_url=image_url if image_url else None,
video_url=video_url if video_url else None,
user_guid=user_guid,
Expand Down Expand Up @@ -226,3 +231,47 @@ def verify_subscriber(email, phone):
return PHONE_TAKEN

return VALID


def stories_amount():
"""
Returns the number of stories. Can be used to find the ID of the last story.

:return: The number of stories
"""
return len(Stories.query.all())


def current_story_id():
"""
The ID of the current story, which is not yet existent, is one greater than the number of stories there is already.

:return: The ID of the current to-be-created story.
"""
return stories_amount() + 1


def get_story_image(story_id):
"""
Creates an SAS key used to acquire a temporary URL to the story image. The SAS key is set to expire in an hour.

:return: A URL of the image of the story
"""
story = Stories.query.filter_by(id=story_id).one()

sas_token = generate_blob_sas(
account_name=current_app.config['AZURE_STORAGE_ACCOUNT_NAME'],
account_key=current_app.config['AZURE_STORAGE_ACCOUNT_KEY'],
container_name=current_app.config['AZURE_CONTAINER_NAME'],
permission=BlobSasPermissions(read=True),
expiry=datetime.utcnow() + timedelta(hours=1),
blob_name=story.image_blob_name
)
image_url = "https://{0}.blob.core.windows.net/{1}/{2}?{3}".format(
current_app.config["AZURE_STORAGE_ACCOUNT_NAME"],
current_app.config["AZURE_CONTAINER_NAME"],
story.image_blob_name,
sas_token,
)

return image_url
13 changes: 12 additions & 1 deletion app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from app.main import main
from app.models import Stories, FeaturedStories
from app.constants import STORY_GOAL_NUMBER
from app.lib.utils import get_story_image
from operator import attrgetter


Expand All @@ -22,10 +23,20 @@ def index():

visible_featured_stories = [str(n+1) for n in range(len(sorted_stories))]

images = []
for story in stories:
if story.image_blob_name != None:
images.append(get_story_image(story.id))
elif story.image_url != None:
images.append(story.image_url)
else:
images.append("")


return render_template('main/home.html',
visible_stories=visible_stories,
remaining_stories=remaining_stories,
stories=stories,
stories=zip(stories, images),
featured_stories=sorted_stories,
visible_featured_stories=visible_featured_stories)

Expand Down
10 changes: 7 additions & 3 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from app import db, es
from app import db
from app.constants import (
permission,
role_name,
Expand Down Expand Up @@ -217,6 +217,7 @@ class Stories(db.Model):
activist_end - an integer containing the activist's death year. If the user wrote "Today", set this value to 9999
content - a string containing the story about the activist
activist_url - a string containing a link to additional information about the activist
image_blob_name - a string containing the name of the image blob on azure
image_url - a string containing a link to an image of the activist
video_url - a string containing a link to a video about the activist
poster_id - an integer containing the id of the user who wrote the story
Expand All @@ -235,6 +236,7 @@ class Stories(db.Model):
activist_end = db.Column(db.Integer)
content = db.Column(db.Text, nullable=False)
activist_url = db.Column(db.Text)
image_blob_name = db.Column(db.Text)
image_url = db.Column(db.Text)
video_url = db.Column(db.Text)
user_guid = db.Column(db.String(64), db.ForeignKey("users.guid"))
Expand All @@ -253,6 +255,7 @@ def __init__(
activist_start=None,
activist_end=None,
activist_url=None,
image_blob_name=None,
image_url=None,
video_url=None,
user_guid=None,
Expand All @@ -264,6 +267,7 @@ def __init__(
self.activist_end = activist_end
self.content = content
self.activist_url = activist_url
self.image_blob_name = image_blob_name
self.image_url = image_url
self.video_url = video_url
self.user_guid = user_guid
Expand All @@ -290,7 +294,7 @@ def val_for_events(self):

def es_create(self):
"""Create elasticsearch doc"""
es.create(
current_app.elasticsearch.create(
index=current_app.config["ELASTICSEARCH_INDEX"],
doc_type='story',
id=self.id,
Expand All @@ -305,7 +309,7 @@ def es_create(self):
)

def es_update(self):
es.update(
current_app.elasticsearch.update(
index=current_app.config["ELASTICSEARCH_INDEX"],
doc_type='story',
id=self.id,
Expand Down
Loading