Manage ASL magazines and their articles.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
asl-articles/asl_articles/articles.py

282 lines
10 KiB

""" Handle article requests. """
import datetime
import base64
import logging
from flask import request, jsonify, abort
from sqlalchemy.sql.expression import func
from asl_articles import app, db
from asl_articles.models import Article, Author, ArticleAuthor, Scenario, ArticleScenario, ArticleImage
from asl_articles.authors import get_author_vals
from asl_articles.scenarios import get_scenario_vals
import asl_articles.publications
import asl_articles.publishers
from asl_articles import search
from asl_articles.utils import get_request_args, clean_request_args, clean_tags, encode_tags, decode_tags, \
apply_attrs, make_ok_response
_logger = logging.getLogger( "db" )
_FIELD_NAMES = [ "*article_title", "article_subtitle", "article_date", "article_snippet", "article_pageno",
"article_url", "article_tags", "pub_id", "publ_id"
]
# ---------------------------------------------------------------------
@app.route( "/article/<article_id>" )
def get_article( article_id ):
"""Get an article."""
_logger.debug( "Get article: id=%s", article_id )
article = Article.query.get( article_id )
if not article:
abort( 404 )
_logger.debug( "- %s", article )
deep = request.args.get( "deep" )
return jsonify( get_article_vals( article, deep ) )
def get_article_vals( article, deep ):
"""Extract public fields from an Article record."""
authors = sorted( article.article_authors,
key = lambda a: a.seq_no
)
scenarios = sorted( article.article_scenarios,
key = lambda a: a.seq_no
)
vals = {
"_type": "article",
"article_id": article.article_id,
"article_title": article.article_title,
"article_subtitle": article.article_subtitle,
"article_image_id": article.article_id if article.article_image else None,
"article_authors": [ get_author_vals( a.parent_author ) for a in authors ],
"article_date": article.article_date,
"article_snippet": article.article_snippet,
"article_pageno": article.article_pageno,
"article_url": article.article_url,
"article_scenarios": [ get_scenario_vals( s.parent_scenario ) for s in scenarios ],
"article_tags": decode_tags( article.article_tags ),
"article_rating": article.article_rating,
"pub_id": article.pub_id,
"publ_id": article.publ_id,
}
if deep:
vals["_parent_pub"] = asl_articles.publications.get_publication_vals(
article.parent_pub, False, False
) if article.parent_pub else None
vals["_parent_publ"] = asl_articles.publishers.get_publisher_vals(
article.parent_publ, False, False
) if article.parent_publ else None
return vals
def get_article_sort_key( article ):
"""Get an article's sort key."""
# NOTE: This is used to sort articles within their parent publication.
# NOTE: Articles should always have a seq# but sometimes they might not (e.g. created via a fixture).
return 999 if article.article_seqno is None else article.article_seqno
# ---------------------------------------------------------------------
@app.route( "/article/create", methods=["POST"] )
def create_article():
"""Create an article."""
# parse the input
vals = get_request_args( request.json, _FIELD_NAMES,
log = ( _logger, "Create article:" )
)
warnings = []
clean_request_args( vals, _FIELD_NAMES, warnings, _logger )
# NOTE: Tags are stored in the database using \n as a separator, so we need to encode *after* cleaning them.
cleaned_tags = clean_tags( vals.get("article_tags"), warnings )
vals[ "article_tags" ] = encode_tags( cleaned_tags )
# create the new article
vals[ "time_created" ] = datetime.datetime.now()
if not vals.get( "publ_id" ):
vals.pop( "article_date", None )
article = Article( **vals )
db.session.add( article )
db.session.flush()
new_article_id = article.article_id
_set_seqno( article, article.pub_id )
_save_authors( article )
_save_scenarios( article )
_save_image( article )
db.session.commit()
_logger.debug( "- New ID: %d", new_article_id )
search.add_or_update_article( None, article, None )
# generate the response
vals = get_article_vals( article, True )
return make_ok_response( record=vals, warnings=warnings )
def _set_seqno( article, pub_id ):
"""Set an article's seq#."""
if pub_id:
max_seqno = db.session.query( func.max( Article.article_seqno ) ) \
.filter( Article.pub_id == pub_id ) \
.scalar()
article.article_seqno = 1 if max_seqno is None else max_seqno+1
else:
article.article_seqno = None
def _save_authors( article ):
"""Save the article's authors."""
# delete the existing article-author rows
db.session.query( ArticleAuthor ) \
.filter( ArticleAuthor.article_id == article.article_id ) \
.delete()
# add the article-author rows
authors = request.json.get( "article_authors", [] )
for seq_no,author in enumerate( authors ):
if isinstance( author, int ):
# this is an existing author
author_id = author
else:
# this is a new author - create it
if not isinstance( author, str ):
raise RuntimeError( "Expected an author name: {}".format( author ) )
author = Author( author_name=author )
db.session.add( author )
db.session.flush()
author_id = author.author_id
_logger.debug( "Created new author \"%s\": id=%d", author, author_id )
db.session.add(
ArticleAuthor( seq_no=seq_no, article_id=article.article_id, author_id=author_id )
)
def _save_scenarios( article ):
"""Save the article's scenarios."""
# delete the existing article-scenario rows
db.session.query( ArticleScenario ) \
.filter( ArticleScenario.article_id == article.article_id ) \
.delete()
# add the article-scenario rows
scenarios = request.json.get( "article_scenarios", [] )
for seq_no,scenario in enumerate( scenarios ):
if isinstance( scenario, int ):
# this is an existing scenario
scenario_id = scenario
else:
# this is a new scenario - create it
if not isinstance( scenario, list ):
raise RuntimeError( "Expected a scenario ID and name: {}".format( scenario ) )
new_scenario = Scenario( scenario_display_id=scenario[0], scenario_name=scenario[1] )
db.session.add( new_scenario )
db.session.flush()
scenario_id = new_scenario.scenario_id
_logger.debug( "Created new scenario \"%s [%s]\": id=%d", scenario[1], scenario[0], scenario_id )
db.session.add(
ArticleScenario( seq_no=seq_no, article_id=article.article_id, scenario_id=scenario_id )
)
def _save_image( article ):
"""Save the article's image."""
# check if a new image was provided
image_data = request.json.get( "imageData" )
if not image_data:
return
# yup - delete the old one from the database
ArticleImage.query.filter( ArticleImage.article_id == article.article_id ).delete()
if image_data == "{remove}":
# NOTE: The front-end sends this if it wants the article to have no image.
article.article_image_id = None
return
# add the new image to the database
image_data = base64.b64decode( image_data )
fname = request.json.get( "imageFilename" )
img = ArticleImage( article_id=article.article_id, image_filename=fname, image_data=image_data )
db.session.add( img )
db.session.flush()
_logger.debug( "Created new image: %s, #bytes=%d", fname, len(image_data) )
# ---------------------------------------------------------------------
@app.route( "/article/update", methods=["POST"] )
def update_article():
"""Update an article."""
# parse the input
article_id = request.json[ "article_id" ]
vals = get_request_args( request.json, _FIELD_NAMES,
log = ( _logger, "Update article: id={}".format( article_id ) )
)
warnings = []
clean_request_args( vals, _FIELD_NAMES, warnings, _logger )
# NOTE: Tags are stored in the database using \n as a separator, so we need to encode *after* cleaning them.
cleaned_tags = clean_tags( vals.get("article_tags"), warnings )
vals[ "article_tags" ] = encode_tags( cleaned_tags )
# update the article
article = Article.query.get( article_id )
if not article:
abort( 404 )
if vals["pub_id"] != article.pub_id:
_set_seqno( article, vals["pub_id"] )
vals[ "time_updated" ] = datetime.datetime.now()
apply_attrs( article, vals )
if not vals.get( "publ_id" ):
article.article_date = None
_save_authors( article )
_save_scenarios( article )
_save_image( article )
db.session.commit()
search.add_or_update_article( None, article, None )
# generate the response
vals = get_article_vals( article, True )
return make_ok_response( record=vals, warnings=warnings )
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@app.route( "/article/update-rating", methods=["POST"] )
def update_article_rating():
"""Update an article's rating."""
# parse the input
article_id = request.json[ "article_id" ]
new_rating = int( request.json[ "rating" ] )
if new_rating < 0 or new_rating > 3:
raise ValueError( "Invalid rating." )
# update the article's rating
article = Article.query.get( article_id )
if not article:
abort( 404 )
article.article_rating = new_rating
db.session.commit()
search.add_or_update_article( None, article, None )
return "OK"
# ---------------------------------------------------------------------
@app.route( "/article/delete/<article_id>" )
def delete_article( article_id ):
"""Delete an article."""
# parse the input
_logger.debug( "Delete article: id=%s", article_id )
article = Article.query.get( article_id )
if not article:
abort( 404 )
_logger.debug( "- %s", article )
# delete the article
db.session.delete( article )
db.session.commit()
search.delete_articles( [ article ] )
# generate the response
return make_ok_response()