Skip to content

Commit

Permalink
few tweaks to short text editing; script to download sample images by…
Browse files Browse the repository at this point in the history
… scraping a given URL
  • Loading branch information
Jaza committed Oct 8, 2015
1 parent d230322 commit 7da22b2
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 32 deletions.
4 changes: 4 additions & 0 deletions flask_editablesite/contentblock/models.py
Expand Up @@ -33,6 +33,10 @@ def __repr__(self):
def default_content(cls):
ret = {}

title = 'Site welcome prefix'
slug = slugify(title, to_lower=True)
ret[slug] = cls(title=title, slug=slug, content='Welcome to', active=True)

title = 'Site byline'
slug = slugify(title, to_lower=True)
ret[slug] = cls(title=title, slug=slug, content='A template for building a small marketing web site in Flask where all content is live editable.', active=True)
Expand Down
90 changes: 90 additions & 0 deletions flask_editablesite/editable/commands.py
@@ -0,0 +1,90 @@
import os
from urlparse import urlparse

from flask import current_app as app
from flask_script import Command, Option, prompt


class DownloadSampleImages(Command):
"""Downloads sample images by scraping a specified URL for links."""

def get_options(self):
return (
Option('--url',
help='URL of web site to scrape for images'),
Option('--targetdir',
help='Path to target directory'),
Option('--parentelname',
help='Parent element name (optional)'),
Option('--parentelclass',
help='Parent element class (optional)'),
Option('--onlyfirstel',
help='Only get the first element of each parent (optional)',
default=False),
)

def download_sample_images(self, url, targetdir, parentelname, parentelclass, onlyfirstel):
from bs4 import BeautifulSoup
import requests

from clint.textui import progress

r = requests.get(url)
soup = BeautifulSoup(r.text, 'lxml')
hrefs = []

if parentelname:
soup_kwargs = {}

if parentelclass:
soup_kwargs['class_'] = parentelclass

for parent_el in soup.find_all(parentelname, **soup_kwargs):
if onlyfirstel:
link = next(iter(parent_el.find_all('a'))).get('href')
hrefs.append(link)
else:
for link in parent_el.find_all('a'):
hrefs.append(link.get('href'))
else:
for link in soup.find_all('a'):
hrefs.append(link.get('href'))

target_filepath = os.path.abspath(targetdir)
if not os.path.exists(target_filepath):
os.makedirs(target_filepath)

total_downloaded = 0

for href in progress.bar(hrefs):
r = requests.get(href, stream=True)
filename = os.path.basename(urlparse(href).path)
filepath = os.path.join(target_filepath, filename)

with open(filepath, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)

total_downloaded += 1

return 'Downloaded %d images' % total_downloaded

def run(self, url=None, targetdir=None, parentelname=None, parentelclass=None, onlyfirstel=False):
if not url:
url = prompt("URL of web site to scrape for images")

if not targetdir:
targetdir = prompt("Path to target directory")

msg = self.download_sample_images(
url=url,
targetdir=targetdir,
parentelname=parentelname,
parentelclass=parentelclass,
onlyfirstel=onlyfirstel)

if not msg:
msg = "Error downloading images"

print(msg)
8 changes: 7 additions & 1 deletion flask_editablesite/editable/forms.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from flask_wtf import Form
from wtforms import TextField, TextAreaField
from wtforms.validators import DataRequired

from flask_wtf import Form
from flask_wtf.file import FileField, FileAllowed


class TextEditForm(Form):
content = TextField('Content', validators=[DataRequired()])
Expand All @@ -18,3 +20,7 @@ class LongTextEditForm(Form):

class LongTextOptionalEditForm(Form):
content = TextAreaField('Content', validators=[])


class ImageEditForm(Form):
image = FileField('Image', validators=[FileAllowed(('gif', 'jpg', 'jpeg', 'png'), 'Only image files (gif, jpg, png) can be uploaded for this field')])
121 changes: 105 additions & 16 deletions flask_editablesite/editable/views.py
Expand Up @@ -8,12 +8,35 @@
from flask_login import login_required, current_user

from flask_editablesite.extensions import db
from flask_editablesite.editable.forms import TextEditForm, LongTextEditForm
from flask_editablesite.editable.forms import TextEditForm, LongTextEditForm, ImageEditForm


blueprint = Blueprint('editable', __name__, static_folder="../static")


def get_model_class(model_classpath, model_name):
"""Dynamically imports the model with the specified classpath."""

model_classpath_format = r'^[a-z0-9_]+(\.[A-Za-z0-9_]+)+$'

if not re.match(model_classpath_format, model_classpath):
raise ValueError('Class path "%s" for model name "%s" must be a valid Python module / class path (in the format "%s")' % (model_classpath, model_name, model_classpath_format))

model_classpath_split = model_classpath.rpartition('.')
model_modulepath, model_classname = (model_classpath_split[0], model_classpath_split[2])

try:
model_module = importlib.import_module(model_modulepath)
except ImportError:
raise ValueError('Error importing module "%s" for model name "%s"' % (model_modulepath, model_name))

model_class = getattr(model_module, model_classname, None)
if not model_class:
raise ValueError('Class "%s" not found in module "%s" for model name "%s"' % (model_classname, model_modulepath, model_name))

return model_class


def text_update_func(model_name, field_name, model_identifier, is_autosave=False):
try:
v = app.config['EDITABLE_MODELS'][model_name]
Expand Down Expand Up @@ -42,21 +65,7 @@ def text_update_func(model_name, field_name, model_identifier, is_autosave=False
except KeyError:
raise ValueError('No title field defined in app config\'s EDITABLE_MODELS for model name "%s"' % model_name)

model_classpath_format = r'^[a-z0-9_]+(\.[A-Za-z0-9_]+)+$'
if not re.match(model_classpath_format, model_classpath):
raise ValueError('Class path "%s" for model name "%s" must be a valid Python module / class path (in the format "%s")' % (model_classpath, model_name, model_classpath_format))

model_classpath_split = model_classpath.rpartition('.')
model_modulepath, model_classname = (model_classpath_split[0], model_classpath_split[2])

try:
model_module = importlib.import_module(model_modulepath)
except ImportError:
raise ValueError('Error importing module "%s" for model name "%s"' % (model_modulepath, model_name))

model_class = getattr(model_module, model_classname, None)
if not model_class:
raise ValueError('Class "%s" not found in module "%s" for model name "%s"' % (model_classname, model_modulepath, model_name))
model_class = get_model_class(model_classpath, model_name)

filter_by_kwargs = {
identifier_field_name: model_identifier,
Expand Down Expand Up @@ -139,3 +148,83 @@ def text_update_autosave(model_name, field_name, model_identifier):
field_name=field_name,
model_identifier=model_identifier,
is_autosave=True)


def image_update_func(model_name, field_name, model_identifier, is_dropzone=False):
try:
v = app.config['EDITABLE_MODELS'][model_name]
except KeyError:
abort(404)

if not(('image_fields' in v) and (field_name in v['image_fields'])):
abort(404)

try:
model_classpath = v['classpath']
except KeyError:
raise ValueError('No class path defined in app config\'s EDITABLE_MODELS for model name "%s"' % model_name)

try:
identifier_field_name = v['identifier_field']
except KeyError:
raise ValueError('No identifier field defined in app config\'s EDITABLE_MODELS for model name "%s"' % model_name)

try:
title_field_name = v['title_field']
except KeyError:
raise ValueError('No title field defined in app config\'s EDITABLE_MODELS for model name "%s"' % model_name)

model_class = get_model_class(model_classpath, model_name)

filter_by_kwargs = {
identifier_field_name: model_identifier,
'active': True}
model = None

if app.config.get('USE_SESSIONSTORE_NOT_DB'):
model_dict = (session.get(model_name, {})
.get(model_identifier, None))

if model_dict:
model = model_class(**model_dict)
else:
model = (model_class.query
.filter_by(**filter_by_kwargs)
.first())

if not model:
try:
model = model_class.default_content()[model_identifier]
except KeyError:
abort(404)

if request.files:
request.form = request.form.copy()
request.form.update(request.files)

form = ImageEditForm()

if form.validate_on_submit():
image_orig = getattr(model, field_name)

filehandle = form.image.data
parts = os.path.splitext(filehandle.filename)


@blueprint.route("/image-update/<model_name>/<field_name>/<model_identifier>/", methods=["POST"])
@login_required
def image_update(model_name, field_name, model_identifier):
return image_update_func(
model_name=model_name,
field_name=field_name,
model_identifier=model_identifier)


@blueprint.route("/image-update-dropzone/<model_name>/<field_name>/<model_identifier>/", methods=["POST"])
@login_required
def image_update_dropzone(model_name, field_name, model_identifier):
return image_update_func(
model_name=model_name,
field_name=field_name,
model_identifier=model_identifier,
is_dropzone=True)
17 changes: 16 additions & 1 deletion flask_editablesite/static/css/editable.css
Expand Up @@ -8,6 +8,10 @@
margin-bottom: 40px;
}

.form-editable-inline {
display: inline;
}

.title-image-wrapper {
margin-left: auto;
margin-right: auto;
Expand All @@ -21,6 +25,17 @@
color: #999;
}

.btn .edit-icon-wrapper {
top: 0;
left: -2em;
}

h1 .edit-icon-wrapper, .h1 .edit-icon-wrapper {
top: 1em;
left: -2em;
font-size: 18px;
}

.edit-icon-dante {
left: -1.5em;
}
Expand All @@ -31,7 +46,7 @@
}

.form-editable span input[type=text] {
width: 50%;
width: 40%;
}

.form-editable input[type=text], .dante-editable {
Expand Down
10 changes: 8 additions & 2 deletions flask_editablesite/templates/macros/heading.html
@@ -1,6 +1,12 @@
{% macro field(field, field_id='', class='h2', placeholder='Type your title', label_class='control-label hidden', wrap_element_start='', wrap_element_end='') %}

<div class="form-group {% if field.errors %} has-error{% endif %}">
{% if wrap_element_start == '<span>' %}
{% set wrap_el_type = 'span' %}
{% else %}
{% set wrap_el_type = 'div' %}
{% endif %}

<{{ wrap_el_type }} class="form-group {% if field.errors %} has-error{% endif %}">
<label for="{{ field.id }}">{{field.label}}</label>

{%- if wrap_element_start %}
Expand All @@ -24,6 +30,6 @@
{%- elif field.description -%}
<p class="help-block">{{field.description|safe}}</p>
{%- endif %}
</div>
</{{ wrap_el_type }}>
{% endmacro %}

12 changes: 9 additions & 3 deletions flask_editablesite/templates/macros/short_text.html
Expand Up @@ -4,12 +4,18 @@

{% with field_id = (model_name + '-' + field_name + '-' + model_identifier) %}

<form id="short-text-form-{{ field_id }}" method="POST" class="form-editable" action="{{ url_for('editable.text_update', model_name=model_name, field_name=field_name, model_identifier=model_identifier) }}" data-autosave-url="{{ url_for('editable.text_update_autosave', model_name=model_name, field_name=field_name, model_identifier=model_identifier) }}" data-autosave-field-id="{{ field_id }}">
<form id="short-text-form-{{ field_id }}" method="POST" class="form-editable{% if wrap_element_start == '<span>' %} form-editable-inline{% endif %}" action="{{ url_for('editable.text_update', model_name=model_name, field_name=field_name, model_identifier=model_identifier) }}" data-autosave-url="{{ url_for('editable.text_update_autosave', model_name=model_name, field_name=field_name, model_identifier=model_identifier) }}" data-autosave-field-id="{{ field_id }}">
{{ form.hidden_tag() }}

<div class="title-image-wrapper"><div class="editable-wrapper">
{% if wrap_element_start == '<span>' %}
{% set wrap_el_type = 'span' %}
{% else %}
{% set wrap_el_type = 'div' %}
{% endif %}

<{{ wrap_el_type }} class="title-image-wrapper"><{{ wrap_el_type }} class="editable-wrapper">
{{ heading.field(form.content, field_id=field_id, class=class, placeholder=placeholder, wrap_element_start=wrap_element_start, wrap_element_end=wrap_element_end) }}
</div></div>
</{{ wrap_el_type }}></{{ wrap_el_type }}>

<div class="form-editable-button">
<button type="submit" class="btn btn-default">Update</button>
Expand Down

0 comments on commit 7da22b2

Please sign in to comment.