Inital commit since swicthing to python 3.14
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
source = pyramid_blogr
|
||||
omit = pyramid_blogr/test*
|
||||
@@ -0,0 +1,22 @@
|
||||
*.egg
|
||||
*.egg-info
|
||||
*.pyc
|
||||
*$py.class
|
||||
*~
|
||||
*.sqlite
|
||||
.coverage
|
||||
coverage.xml
|
||||
build/
|
||||
dist/
|
||||
.tox/
|
||||
nosetests.xml
|
||||
env*/
|
||||
tmp/
|
||||
Data.fs*
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.*.sw?
|
||||
.sw?
|
||||
.DS_Store
|
||||
coverage
|
||||
test
|
||||
@@ -0,0 +1,4 @@
|
||||
0.1
|
||||
---
|
||||
|
||||
- Initial version.
|
||||
@@ -0,0 +1,2 @@
|
||||
include *.txt *.ini *.cfg *.rst
|
||||
recursive-include ctp_blogr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
|
||||
@@ -0,0 +1,29 @@
|
||||
# README #
|
||||
|
||||
**ctp_blogr** est une application pour créer un blog simple.
|
||||
|
||||
## Fonctionnalités ##
|
||||
|
||||
Bien que **ctp_blogr** soit une application minimale et simple, elle possède toutes les fonctions nécessaire pour gérer un blog :
|
||||
|
||||
- Gestion des utilisateurs du blog (admin ou rédacteur)
|
||||
- Authentification et autorisation des utilisateurs
|
||||
- Gestion des posts du blog avec des statuts : *publié, brouillon ou privé*
|
||||
- Gestion des tags (un post peut avoir un tag)
|
||||
- Les posts sont rédigé avec le langage *Markdown*
|
||||
- Possibilité de faire des recherches sur le titre et le corps des posts
|
||||
|
||||
## Add-on requis ##
|
||||
|
||||
- Python 3.9.6
|
||||
- Pyramid 2.0.2
|
||||
- SQLite 3.43.2
|
||||
- pyramid-jinja2 2.7 : view templating
|
||||
- wtforms 3.2.1 : form library
|
||||
- webhelpers2 2.0 : various web building related helpers
|
||||
- Markdown 3.4.1 :
|
||||
|
||||
|
||||
### 1.0 (22.01.2023)
|
||||
|
||||
- version initiale
|
||||
@@ -0,0 +1,27 @@
|
||||
from pyramid.config import Configurator
|
||||
from pyramid.authentication import AuthTktAuthenticationPolicy
|
||||
from pyramid.authorization import ACLAuthorizationPolicy
|
||||
from pyramid.session import SignedCookieSessionFactory
|
||||
|
||||
from .services.user import groupfinder
|
||||
|
||||
|
||||
def main(global_config, **settings):
|
||||
""" This function returns a Pyramid WSGI application.
|
||||
"""
|
||||
# session factory
|
||||
my_session_factory = SignedCookieSessionFactory('hZug2zPt7hT2MZ')
|
||||
|
||||
authentication_policy = AuthTktAuthenticationPolicy('J2wv322aL5DTn2',
|
||||
callback=groupfinder, hashalg='sha512', timeout=36000)
|
||||
authorization_policy = ACLAuthorizationPolicy()
|
||||
with Configurator(settings=settings,
|
||||
root_factory='ctp_blogr.security.RootFactory',
|
||||
authentication_policy=authentication_policy,
|
||||
authorization_policy=authorization_policy) as config:
|
||||
config.include('pyramid_jinja2')
|
||||
config.include('.models')
|
||||
config.include('.routes')
|
||||
config.set_session_factory(my_session_factory)
|
||||
config.scan()
|
||||
return config.make_wsgi_app()
|
||||
@@ -0,0 +1,66 @@
|
||||
from wtforms import Form, StringField, TextAreaField, SelectField, DecimalField
|
||||
from wtforms import IntegerField, PasswordField
|
||||
from wtforms.validators import InputRequired, Length, EqualTo
|
||||
from wtforms.widgets import HiddenInput
|
||||
|
||||
strip_filter = lambda x: x.strip() if x else None
|
||||
|
||||
class BlogCreateForm(Form):
|
||||
title = StringField('Titre', validators=[InputRequired(), Length(min=1, max=255)],
|
||||
filters=[strip_filter])
|
||||
body = TextAreaField('Corps du texte', validators=[InputRequired(), Length(min=1)],
|
||||
filters=[strip_filter])
|
||||
tag = SelectField('Tag')
|
||||
status = SelectField('Statut', choices=[('brouillon','Brouillon'),('privé','Privé'),('publié','Publié')])
|
||||
|
||||
class BlogUpdateForm(BlogCreateForm):
|
||||
id = IntegerField(widget=HiddenInput())
|
||||
|
||||
class BlogSearchForm(Form):
|
||||
criteria = StringField('Critère', validators=[InputRequired(), Length(min=3, max=45)],
|
||||
filters=[strip_filter])
|
||||
|
||||
class TagForm(Form):
|
||||
id = IntegerField(widget=HiddenInput())
|
||||
tag = StringField('Tag', validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter])
|
||||
|
||||
|
||||
class UserCreateForm(Form):
|
||||
id = IntegerField(widget=HiddenInput())
|
||||
name = StringField('Nom', validators=[InputRequired(), Length(min=1, max=255)],
|
||||
filters=[strip_filter])
|
||||
password = PasswordField('Mot de passe')
|
||||
confirm = PasswordField('Confirmer', validators=[EqualTo('password', message='Les 2 Passwords doivent être identiques')])
|
||||
|
||||
class HistoForm(Form):
|
||||
no_id = IntegerField(widget=HiddenInput())
|
||||
mvt_cash = DecimalField('Montant à ajouter au portefeuille', places=2, validators=[InputRequired()])
|
||||
|
||||
class AllocationForm(Form):
|
||||
no_cat = IntegerField(widget=HiddenInput())
|
||||
classe = StringField("Classe d'actif", validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter])
|
||||
pc_cible = IntegerField(validators=[InputRequired()])
|
||||
type = SelectField('Type', choices=[('ACTION','ACTION'),('AUTRE','AUTRE')])
|
||||
ordre = IntegerField(validators=[InputRequired()])
|
||||
bg_color = SelectField('Couleur de fond', choices=[('info','BLEU'),('danger','ROUGE'),('warning','ORANGE'),('success','VERT')])
|
||||
|
||||
class ActifForm(Form):
|
||||
no_id = IntegerField(widget=HiddenInput())
|
||||
classe = SelectField('Classe')
|
||||
symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter])
|
||||
libelle = StringField('Nom', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter])
|
||||
nombre = IntegerField(validators=[InputRequired()])
|
||||
pru = DecimalField('PRU', places=3, validators=[InputRequired()])
|
||||
ter = DecimalField('TER', places=2, validators=[InputRequired()])
|
||||
pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()])
|
||||
website = StringField('Web site', validators=[Length(min=1, max=100)], filters=[strip_filter])
|
||||
|
||||
class Actif2Form(Form):
|
||||
no_id = IntegerField(widget=HiddenInput())
|
||||
classe = SelectField('Classe')
|
||||
symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter])
|
||||
libelle = StringField('Libellé', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter])
|
||||
pru = DecimalField('Total investi', places=3, validators=[InputRequired()])
|
||||
cours = DecimalField('Total valeur', places=3, validators=[InputRequired()])
|
||||
pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()])
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
import zope.sqlalchemy
|
||||
|
||||
# import or define all models here to ensure they are attached to the
|
||||
# Base.metadata prior to any initialization routines
|
||||
from .user import User
|
||||
from .blog_record import BlogRecord
|
||||
|
||||
# run configure_mappers after defining all of the models to ensure
|
||||
# all relationships can be setup
|
||||
configure_mappers()
|
||||
|
||||
|
||||
def get_engine(settings, prefix='sqlalchemy.'):
|
||||
return engine_from_config(settings, prefix)
|
||||
|
||||
|
||||
def get_session_factory(engine):
|
||||
factory = sessionmaker()
|
||||
factory.configure(bind=engine)
|
||||
return factory
|
||||
|
||||
|
||||
def get_tm_session(session_factory, transaction_manager):
|
||||
"""
|
||||
Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
|
||||
|
||||
This function will hook the session to the transaction manager which
|
||||
will take care of committing any changes.
|
||||
|
||||
- When using pyramid_tm it will automatically be committed or aborted
|
||||
depending on whether an exception is raised.
|
||||
|
||||
- When using scripts you should wrap the session in a manager yourself.
|
||||
For example::
|
||||
|
||||
import transaction
|
||||
|
||||
engine = get_engine(settings)
|
||||
session_factory = get_session_factory(engine)
|
||||
with transaction.manager:
|
||||
dbsession = get_tm_session(session_factory, transaction.manager)
|
||||
|
||||
"""
|
||||
dbsession = session_factory()
|
||||
zope.sqlalchemy.register(
|
||||
dbsession, transaction_manager=transaction_manager)
|
||||
return dbsession
|
||||
|
||||
|
||||
def includeme(config):
|
||||
"""
|
||||
Initialize the model for a Pyramid app.
|
||||
|
||||
Activate this setup using ``config.include('ctp_blogr.models')``.
|
||||
|
||||
"""
|
||||
settings = config.get_settings()
|
||||
settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
|
||||
|
||||
# use pyramid_tm to hook the transaction lifecycle to the request
|
||||
config.include('pyramid_tm')
|
||||
|
||||
# use pyramid_retry to retry a request when transient exceptions occur
|
||||
config.include('pyramid_retry')
|
||||
|
||||
session_factory = get_session_factory(get_engine(settings))
|
||||
config.registry['dbsession_factory'] = session_factory
|
||||
|
||||
# make request.dbsession available for use in Pyramid
|
||||
config.add_request_method(
|
||||
# r.tm is the transaction manager used by pyramid_tm
|
||||
lambda r: get_tm_session(session_factory, r.tm),
|
||||
'dbsession',
|
||||
reify=True
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
import datetime #<- will be used to set default dates on models
|
||||
from ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
Unicode, #<- will provide Unicode field
|
||||
UnicodeText, #<- will provide Unicode text field
|
||||
DateTime, #<- time abstraction field
|
||||
)
|
||||
from webhelpers2.text import urlify #<- will generate slugs
|
||||
from webhelpers2.date import distance_of_time_in_words #<- human friendly dates
|
||||
|
||||
|
||||
class BlogRecord(Base):
|
||||
__tablename__ = 'entries'
|
||||
id = Column(Integer, primary_key=True)
|
||||
title = Column(Unicode(255), unique=True, nullable=False)
|
||||
body = Column(UnicodeText, default='')
|
||||
created = Column(DateTime, default=datetime.datetime.now)
|
||||
creator = Column(Unicode(50), default='')
|
||||
edited = Column(DateTime, default=datetime.datetime.now)
|
||||
editor = Column(Unicode(50), default='')
|
||||
tag = Column(Unicode(25))
|
||||
status = Column(Unicode(50), default='brouillon')
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
return urlify(self.title)
|
||||
|
||||
@property
|
||||
def created_in_words(self):
|
||||
return distance_of_time_in_words(self.created,
|
||||
datetime.datetime.utcnow())
|
||||
|
||||
|
||||
class Tags(Base):
|
||||
__tablename__ = 'tags'
|
||||
id = Column(Integer, primary_key=True)
|
||||
tag = Column(Unicode(25))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.schema import MetaData
|
||||
|
||||
# Recommended naming convention used by Alembic, as various different database
|
||||
# providers will autogenerate vastly different names making migrations more
|
||||
# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
|
||||
NAMING_CONVENTION = {
|
||||
"ix": "ix_%(column_0_label)s",
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s"
|
||||
}
|
||||
|
||||
metadata = MetaData(naming_convention=NAMING_CONVENTION)
|
||||
Base = declarative_base(metadata=metadata)
|
||||
@@ -0,0 +1,34 @@
|
||||
import datetime #<- will be used to set default dates on models
|
||||
from ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
Unicode, #<- will provide Unicode field
|
||||
UnicodeText, #<- will provide Unicode text field
|
||||
DateTime, #<- time abstraction field
|
||||
)
|
||||
|
||||
from passlib.apps import custom_app_context as blogger_pwd_context
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(Unicode(255), unique=True, nullable=False)
|
||||
password = Column(Unicode(255), nullable=False)
|
||||
last_logged = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def verify_password(self, password):
|
||||
# is it cleartext?
|
||||
if password == self.password:
|
||||
self.set_password(password)
|
||||
# verify password
|
||||
result = blogger_pwd_context.verify(password, self.password)
|
||||
if result:
|
||||
# pwd OK, set last login date
|
||||
self.last_logged = datetime.datetime.now()
|
||||
return result
|
||||
|
||||
def set_password(self, password):
|
||||
password_hash = blogger_pwd_context.encrypt(password)
|
||||
self.password = password_hash
|
||||
@@ -0,0 +1,13 @@
|
||||
from . import models
|
||||
|
||||
|
||||
def setup(env):
|
||||
request = env['request']
|
||||
|
||||
# start a transaction
|
||||
request.tm.begin()
|
||||
|
||||
# inject some vars into the shell builtins
|
||||
env['tm'] = request.tm
|
||||
env['dbsession'] = request.dbsession
|
||||
env['models'] = models
|
||||
@@ -0,0 +1,15 @@
|
||||
def includeme(config):
|
||||
config.add_static_view('static', 'static', cache_max_age=3600)
|
||||
config.add_route('home', '/')
|
||||
config.add_route('apropos', '/apropos')
|
||||
config.add_route('blog', '/blog/{id:\d+}/{slug}')
|
||||
config.add_route('blog_bytag', '/blog_bytag/{tag}/{retour}')
|
||||
config.add_route('blog_edit', '/blog_edit/{id}')
|
||||
config.add_route('blog_search', '/blog_search')
|
||||
config.add_route('login', '/login')
|
||||
config.add_route('logout', '/logout')
|
||||
config.add_route('tags', '/tags')
|
||||
config.add_route('tag_edit', '/tag_edit/{id}')
|
||||
config.add_route('users', '/users')
|
||||
config.add_route('user_edit', '/user_edit/{name}')
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# package
|
||||
@@ -0,0 +1,15 @@
|
||||
from pyramid.security import (
|
||||
Allow,
|
||||
Authenticated,
|
||||
DENY_ALL,
|
||||
)
|
||||
|
||||
class RootFactory(object):
|
||||
"""Defines an ACL for groups/permissions mapping"""
|
||||
__acl__ = [ (Allow, Authenticated, 'view'),
|
||||
(Allow, 'group:administrators', 'manage'),
|
||||
DENY_ALL,
|
||||
]
|
||||
def __init__(self, request):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import sqlalchemy as sa
|
||||
import datetime #<- will be used to set default dates on models
|
||||
|
||||
from sqlalchemy import or_
|
||||
from ..models.blog_record import BlogRecord, Tags
|
||||
|
||||
|
||||
|
||||
class BlogRecordService(object):
|
||||
|
||||
@classmethod
|
||||
def by_criteria(cls, request, criteria):
|
||||
search = "%{}%".format(criteria)
|
||||
query = request.dbsession.query(BlogRecord)
|
||||
query = query.filter(or_(BlogRecord.title.like(search), BlogRecord.body.like(search)))
|
||||
if request.authenticated_userid == None:
|
||||
# if user is anonym, display only published posts
|
||||
query = query.filter(BlogRecord.status == 'publié')
|
||||
return query.all()
|
||||
|
||||
@classmethod
|
||||
def by_id(cls, request, _id):
|
||||
query = request.dbsession.query(BlogRecord)
|
||||
return query.get(_id)
|
||||
|
||||
@classmethod
|
||||
def by_tag(cls, request, tag):
|
||||
query = request.dbsession.query(BlogRecord)
|
||||
query = query.filter(BlogRecord.tag == tag)
|
||||
if request.authenticated_userid == None:
|
||||
# if user is anonym, display only published posts
|
||||
query = query.filter(BlogRecord.status == 'publié')
|
||||
return query.order_by(sa.desc(BlogRecord.created)).all()
|
||||
|
||||
@classmethod
|
||||
def get_last_created(cls, request):
|
||||
# gest the 10 last created posts
|
||||
query = request.dbsession.query(BlogRecord)
|
||||
if request.authenticated_userid == None:
|
||||
# if user is anonym, display only published posts
|
||||
query = query.filter(BlogRecord.status == 'publié')
|
||||
query = query.order_by(sa.desc(BlogRecord.created)).limit(15).all()
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def delete(cls, request, id):
|
||||
request.dbsession.query(BlogRecord).filter(BlogRecord.id == id).delete(synchronize_session=False)
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def get_tags(cls, request):
|
||||
query = request.dbsession.query(Tags)
|
||||
query = query.order_by(Tags.tag).all()
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def get_tags_byId(cls, request, id):
|
||||
# gest the last 5 items modified
|
||||
query = request.dbsession.query(Tags).filter(Tags.id == id).first()
|
||||
return query
|
||||
|
||||
@classmethod
|
||||
def tag_delete(cls, request, id):
|
||||
request.dbsession.query(Tags).filter(Tags.id == id).delete(synchronize_session=False)
|
||||
return
|
||||
@@ -0,0 +1,32 @@
|
||||
import sqlalchemy as sa
|
||||
from ..models.user import User
|
||||
|
||||
|
||||
class UserService(object):
|
||||
|
||||
@classmethod
|
||||
def all(cls, request):
|
||||
items = request.dbsession.query(User).order_by(sa.asc(User.name)).all()
|
||||
return items
|
||||
|
||||
@classmethod
|
||||
def by_name(cls, request, name ):
|
||||
item = request.dbsession.query(User).filter(User.name == name).first()
|
||||
return item
|
||||
|
||||
@classmethod
|
||||
def delete(cls, request, id):
|
||||
request.dbsession.query(User).filter(User.id == id).delete(synchronize_session=False)
|
||||
return
|
||||
|
||||
def groupfinder(userid, request):
|
||||
|
||||
if userid:
|
||||
# user name is 'admin' ?
|
||||
if userid == 'admin':
|
||||
return ['group:administrators']
|
||||
else:
|
||||
return [] # it means that userid is logged in
|
||||
else:
|
||||
# it returns None if userid isn't logged in
|
||||
return None
|
||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,163 @@
|
||||
/* Theme inspired from Bootstrap Theme "The Band" */
|
||||
/* https://www.w3schools.com/bootstrap/bootstrap_theme_band.asp */
|
||||
|
||||
body {
|
||||
font: 100 18px/24px Times New Roman, serif;
|
||||
color: rgb(90, 89, 89);
|
||||
}
|
||||
.container h2, h3{
|
||||
color: #df4937 !important;
|
||||
}
|
||||
h5 {
|
||||
margin: 10px 0 30px 0;
|
||||
letter-spacing: 10px;
|
||||
font-size: 20px;
|
||||
color: #111;
|
||||
}
|
||||
.person {
|
||||
border: 10px solid transparent;
|
||||
margin-bottom: 25px;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.person:hover {
|
||||
border-color: #f1f1f1;
|
||||
}
|
||||
.carousel-inner img {
|
||||
-webkit-filter: grayscale(90%);
|
||||
filter: grayscale(90%); /* make all photos black and white */
|
||||
width: 100%; /* Set width to 100% */
|
||||
margin: auto;
|
||||
}
|
||||
.carousel-caption h3 {
|
||||
color: #fff !important;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.carousel-caption {
|
||||
display: none; /* Hide the carousel text when the screen is less than 600 pixels wide */
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container{
|
||||
max-width: 950px;
|
||||
}
|
||||
}
|
||||
@media print {
|
||||
/* ne pas affichier l'url after the link */
|
||||
a[href]:after {
|
||||
content: none !important;
|
||||
}
|
||||
* {
|
||||
color: inherit !important;
|
||||
background-color: inherit !important;
|
||||
}
|
||||
}
|
||||
.bg-1 {
|
||||
background: #bc2131;
|
||||
color: #bdbdbd;
|
||||
}
|
||||
.bg-1 h3 {color: #fff;}
|
||||
.bg-1 p {font-style: italic;}
|
||||
.list-group-item:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
.list-group-item:last-child {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.thumbnail {
|
||||
padding: 0 0 15px 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.monospace-font {
|
||||
font-family: Monaco, "Courier New", Courier, monospace;
|
||||
}
|
||||
.thumbnail p {
|
||||
margin-top: 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.modal-header, h4, .close {
|
||||
background-color: #333;
|
||||
color: #fff !important;
|
||||
text-align: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
.modal-header, .modal-body {
|
||||
padding: 40px 50px;
|
||||
}
|
||||
.nav-tabs li a {
|
||||
color: #777;
|
||||
}
|
||||
#googleMap {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
.navbar {
|
||||
font-family: Montserrat, sans-serif;
|
||||
margin-bottom: 0;
|
||||
background-color: #bc2131;
|
||||
border: 0;
|
||||
font-size: 14px !important;
|
||||
letter-spacing: 4px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.navbar li a, .navbar .navbar-brand {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.navbar-nav li a:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
.navbar-nav li.active a {
|
||||
color: #fff !important;
|
||||
background-color: #29292c !important;
|
||||
}
|
||||
.navbar-default .navbar-toggle {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: #bc2131;
|
||||
color: #f5f5f5;
|
||||
padding: 32px;
|
||||
}
|
||||
footer a {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
footer a:hover {
|
||||
color: #777;
|
||||
text-decoration: none;
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 0;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
.required-field::after {
|
||||
content: "*";
|
||||
color: red;
|
||||
margin-left:2px;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.open .dropdown-toggle {
|
||||
color: #fff ;
|
||||
background-color: #555 !important;
|
||||
}
|
||||
|
||||
/* Dropdown links */
|
||||
.dropdown-menu li a {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
/* On hover, the dropdown links will turn red */
|
||||
.dropdown-menu li a:hover {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content">
|
||||
<h1><span class="font-semi-bold">CTP Blog</span></h1>
|
||||
<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="panel panel-default text-center">
|
||||
<div class="panel-body">
|
||||
<blockquote>
|
||||
L'argent qu'on possède est l'instrument de la liberté; celui qu'on pourchasse est celui de la servitude.
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<h4>Rousseau</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<div class="panel panel-default text-center">
|
||||
<div class="panel-body">
|
||||
<blockquote>
|
||||
L'intelligence ce n'est pas ce que l'on sait mais ce que l'on fait quand on ne sait pas.
|
||||
<br />
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<h4>Jean Piaget</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
{% if request.authenticated_userid %}
|
||||
<p>
|
||||
<a href="{{ return_url }}">[ Retour ]</a>
|
||||
<a href="{{ request.route_url('blog_edit', id=entry.id) }}">[ Modifier ]</a>
|
||||
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<hr/>
|
||||
<p>{{ body_html | safe }}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
Publié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
|
||||
{% if request.authenticated_userid %}
|
||||
Modifié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
|
||||
Tag : <strong>{{ entry.tag }}</strong><br>
|
||||
Statut : <strong>{{ entry.status }}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<script>
|
||||
const anchors = document.querySelectorAll('a');
|
||||
anchors.forEach((a) => {
|
||||
a.setAttribute('target', '__blank');
|
||||
a.setAttribute('rel', 'noopener noreferrer');
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p><a href="{{ url_retour }}">[ retour ]</a></p>
|
||||
|
||||
<table class="table table-condensed">
|
||||
{% for entry in items %}
|
||||
<tr>
|
||||
<td>{{ entry.created.strftime("%d %b %y") }}</td>
|
||||
<td>
|
||||
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
|
||||
</td>
|
||||
{% if entry.status != 'publié' %}
|
||||
<td><span class="label label-danger">{{ entry.status }}</span></td>
|
||||
{% else %}
|
||||
<td> </td>
|
||||
{% endif%}
|
||||
</tr>
|
||||
{% else %}
|
||||
<p class="text-danger">Aucun post trouvé !</p>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<p class="text-center">[ {{nb_items}} billets ]</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
<form action="{{ url }}" method="post" class="form">
|
||||
|
||||
{% for error in form.title.errors %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="title">{{ form.title.label }}</label>
|
||||
{{ form.title(class_='form-control') }}
|
||||
</div>
|
||||
|
||||
{% for error in form.body.errors %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endfor %}
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="body">{{ form.body.label }}</label>
|
||||
{{ form.body(class_='form-control monospace-font', cols="35", rows="20") }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="tag">{{ form.tag.label }}</label>
|
||||
{{ form.tag(class_='form-control') }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="status">{{ form.status.label }}</label>
|
||||
{{ form.status(class_='form-control') }}
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{% if blog_id != '0' %}
|
||||
Créé le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong>
|
||||
<button class="btn btn-link" type="submit" name="form.update_created">[ Mettre à jour ]</button>
|
||||
<br>
|
||||
Modifié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<div class="form-group">
|
||||
<a class="btn btn-default" href="{{ request.route_url('home') }}"><span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
|
||||
<button class="btn btn-primary" type="submit" name="form.submitted">
|
||||
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
|
||||
{% if blog_id != '0' %}
|
||||
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
|
||||
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="text-center">Apprendre la syntaxe de <a href="https://www.markdownguide.org/basic-syntax/" target="_blank">Markdown</a></li></p>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Modal : Confirmation SUPRESSION -->
|
||||
<div id="confirmDelete" class="modal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Supprimer une page</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- The form is placed inside the body of modal -->
|
||||
<p>Etes-vous certain(e) de vouloir supprimer <b><br>
|
||||
{{ entry.title }}</b> ?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group">
|
||||
<div class="text-center">
|
||||
<form id="confirmForm" method="post">
|
||||
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,57 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
<form id="search-form" class="form-horizontal" role="form" action="/blog_search" method="post">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-8">
|
||||
<div class="input-group" align="center">
|
||||
{{ form.criteria(class_='form-control') }}
|
||||
<span class="input-group-btn">
|
||||
<button id="submitButton" class="btn btn-primary" type="submit" name="form.submitted">
|
||||
Rechercher
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
{% for error in form.criteria.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
{% if items %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titre</th>
|
||||
<th>Tags</th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for entry in items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
|
||||
{{ entry.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ entry.tag }}</td>
|
||||
<td>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
<br />
|
||||
<h2 class="text-center">Index</h2>
|
||||
<p class="text-center">
|
||||
|
|
||||
{% for tag in tags %}
|
||||
<a href="{{ request.route_url('blog_bytag', tag=tag.tag, retour='blog_search') }}">{{ tag.tag }}</a> |
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,43 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div>
|
||||
<h3 class="text-center">{{ cartoon_title }}</h3>
|
||||
<p class="text-center">
|
||||
<img src="{{ cartoon_url }}"
|
||||
class="img_responsive" style="width:440px" /><br>
|
||||
<a href="https://www.chappatte.com/fr/" target="_blank">© Chappatte</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% if request.authenticated_userid %}
|
||||
<p>
|
||||
<a href="{{ request.route_url('blog_edit', id='0') }}">[ Nouveau ]</a>
|
||||
<a href="{{ request.route_url('blog_edit', id='95') }}">[ Sélectionner un autre le dessin ]</a>
|
||||
</p>
|
||||
{% endif%}
|
||||
|
||||
<table id="posts_list" class="table table-condensed">
|
||||
{% for entry in last_ten %}
|
||||
<tr>
|
||||
<td>{{ entry.created.strftime("%d %b %y") }}</td>
|
||||
<td>
|
||||
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
[ <a href="{{ request.route_url('blog_bytag', tag=entry.tag, retour='home') }}">{{ entry.tag }}</a> ]
|
||||
</td>
|
||||
{% if entry.status != 'publié' %}
|
||||
<td><span class="label label-danger">{{ entry.status }}</span></td>
|
||||
{% else %}
|
||||
<td> </td>
|
||||
{% endif%}
|
||||
</tr>
|
||||
{% else %}
|
||||
<p class="text-danger">Aucun post trouvé</p>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="pyramid web application">
|
||||
<link rel="shortcut icon" href="{{request.static_url('ctp_blogr:static/mythosaur_skull_red.ico')}}">
|
||||
|
||||
<title>{{page_title}}</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
|
||||
<!-- Custom styles for this scaffold -->
|
||||
<link href="{{request.static_url('ctp_blogr:static/theme.css')}}" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<nav class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#myNavbar">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="{{ request.route_url('home') }}"><img src="{{request.static_url('ctp_blogr:static/mythosaur_skull_60x150.png')}}" /></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" id="myNavbar">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="{{ request.route_url('blog_search') }}"><span class="glyphicon glyphicon-search"></span></a></li>
|
||||
{% if request.authenticated_userid %}
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">{{request.authenticated_userid}}
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% if request.authenticated_userid == 'admin' %}
|
||||
<li><a href="{{request.route_url('users')}}"><span class="glyphicon glyphicon-user"></span> Utilisateurs</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{request.route_url('user_edit', name=request.authenticated_userid)}}">
|
||||
<span class="glyphicon glyphicon-user"></span> Modifier le mot de passe</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li><a href="{{ request.route_url('tags') }}"><span class="glyphicon glyphicon-tag"></span> Tags</a></li>
|
||||
<li><a href="{{ request.route_url('logout') }}"><span class="glyphicon glyphicon-off"></span> Se déconnecter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
<!-- si anonyme, lien pour se connecter -->
|
||||
<li><a href="{{ request.route_url('login') }}">
|
||||
<span class="glyphicon glyphicon-user"></span></a></li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Container (The Page Template Section) -->
|
||||
<div class="container">
|
||||
<br>
|
||||
<br>
|
||||
<!-- Display Page Title -->
|
||||
{% if page_title %}
|
||||
<h1>{{ page_title }}</h1>
|
||||
{% endif %}
|
||||
<br />
|
||||
<div id="messages">
|
||||
{% for queue in ['', 'info', 'success', 'warning', 'danger'] %}
|
||||
{% for message in request.session.pop_flash(queue) %}
|
||||
<div class="alert alert-{{queue}}">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- display page content-->
|
||||
{% block content %}
|
||||
<p>No content</p>
|
||||
{% endblock content %}
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-center">
|
||||
<div class="row">
|
||||
<p class="text-center">
|
||||
© 2017 - Phuoc Cao
|
||||
 | <a href="{{ request.route_url('apropos')}}">A propos</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap core JavaScript
|
||||
================================================== -->
|
||||
<!-- Placed at the end of the document so the pages load faster -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-offset-4 col-md-5 well">
|
||||
|
||||
<form action="{{ login_url }}" method="post">
|
||||
<h2>Se connecter</h2>
|
||||
<input type="hidden" name="came_from" value="{{ came_from }}" />
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" name="username" class="form-control" placeholder="Identifiant">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" name="password" class="form-control" placeholder="Mot de passe">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" name="form.submitted" value="Se connecter" class="btn btn-primary">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<form action="{{ url }}" method="post" class="form">
|
||||
|
||||
{% for error in form.tag.errors %}
|
||||
<div class="error">{{ error }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="tag">{{form.tag.label}}</label>
|
||||
{{form.tag(class_='form-control')}}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<a class="btn btn-default" href="{{ request.route_url('tags') }}">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
|
||||
<button class="btn btn-primary" type="submit" name="form.submitted">
|
||||
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
|
||||
{% if form.id.data %}
|
||||
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
|
||||
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Modal : Confirmation SUPRESSION -->
|
||||
<div id="confirmDelete" class="modal" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Supprimer le Tag</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- The form is placed inside the body of modal -->
|
||||
<p>Etes-vous certain(e) de vouloir supprimer le Tag <b>{{ form.tag.data }}</b> ?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group">
|
||||
<div class="text-center">
|
||||
<form id="confirmForm" method="post">
|
||||
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p><a href="{{ request.route_url('tag_edit', id='0') }}" class="btn btn-success" role="button">
|
||||
<span class="glyphicon glyphicon-plus"></span> Nouveau</a>
|
||||
</p>
|
||||
|
||||
<table id="users_list" class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No Id</th>
|
||||
<th>Tag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for entry in tags %}
|
||||
<tr>
|
||||
<td>{{ entry.id }}</td>
|
||||
<td>
|
||||
<a href="{{ request.route_url('tag_edit', id=entry.id) }}">{{ entry.tag }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% if message %}
|
||||
<div class="alert alert-danger">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form action="{{ url }}" method="post" class="form">
|
||||
|
||||
{% for error in form.name.errors %}
|
||||
<div class="label label-warning">{{ error }}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="name">{{form.name.label}}</label>
|
||||
{% if form.id.data %}
|
||||
<input class="form-control" name="name" readonly type="text" value="{{form.name.data}}">
|
||||
{% else %}
|
||||
{{form.name(class_='form-control')}}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="password">{{form.password.label}}</label>
|
||||
{{form.password(class_='form-control')}}
|
||||
</div>
|
||||
|
||||
{% for error in form.confirm.errors %}
|
||||
<div class="label label-danger">{{error}}</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="required-field" for="confirm">{{form.confirm.label}}</label>
|
||||
{{form.confirm(class_='form-control')}}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="form-group">
|
||||
<a class="btn btn-default" href="{{ url_retour }}"><span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
|
||||
<button class="btn btn-primary" type="submit" name="form.submitted">
|
||||
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
|
||||
{% if form.id.data and request.authenticated_userid == 'admin' %}
|
||||
<button class="btn btn-warning" type="submit" name="form.deleted">
|
||||
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "layout.jinja2" %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
<a href="{{ request.route_url('home' ) }}" class="btn btn-default" role="button">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
|
||||
<a href="{{ request.route_url('user_edit', name='0') }}" class="btn btn-success" role="button">
|
||||
<span class="glyphicon glyphicon-plus"></span> Nouvel utilisateur</a>
|
||||
</p>
|
||||
|
||||
<table id="users_list" class="table table-striped table-bordered table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>No Id</th>
|
||||
<th>Nom</th>
|
||||
<th>Dernière connexion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for entry in users %}
|
||||
<tr>
|
||||
<td>{{ entry.id }}</td>
|
||||
<td>
|
||||
<a href="{{ request.route_url('user_edit', name=entry.name) }}">
|
||||
{{ entry.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ entry.last_logged.strftime("%d-%m-%Y - %H:%M") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
|
||||
from pyramid import testing
|
||||
|
||||
import transaction
|
||||
|
||||
|
||||
def dummy_request(dbsession):
|
||||
return testing.DummyRequest(dbsession=dbsession)
|
||||
|
||||
|
||||
class BaseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.config = testing.setUp(settings={
|
||||
'sqlalchemy.url': 'sqlite:///:memory:'
|
||||
})
|
||||
self.config.include('.models')
|
||||
settings = self.config.get_settings()
|
||||
|
||||
from .models import (
|
||||
get_engine,
|
||||
get_session_factory,
|
||||
get_tm_session,
|
||||
)
|
||||
|
||||
self.engine = get_engine(settings)
|
||||
session_factory = get_session_factory(self.engine)
|
||||
|
||||
self.session = get_tm_session(session_factory, transaction.manager)
|
||||
|
||||
def init_database(self):
|
||||
from .models.meta import Base
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
||||
def tearDown(self):
|
||||
from .models.meta import Base
|
||||
|
||||
testing.tearDown()
|
||||
transaction.abort()
|
||||
Base.metadata.drop_all(self.engine)
|
||||
|
||||
|
||||
class TestMyViewSuccessCondition(BaseTest):
|
||||
|
||||
def test_passing_view(self):
|
||||
from .views.default import apropos
|
||||
response = apropos(dummy_request(self.session))
|
||||
self.assertEqual(response['page_title'], 'A propos')
|
||||
|
||||
|
||||
class TestMyViewFailureCondition(BaseTest):
|
||||
|
||||
def test_failing_view(self):
|
||||
from .views.default import apropos
|
||||
response = apropos(dummy_request(self.session))
|
||||
self.assertEqual(response['page_title'], 'A propos')
|
||||
@@ -0,0 +1,188 @@
|
||||
from pyramid.view import view_config
|
||||
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
|
||||
from ..models.blog_record import BlogRecord, Tags
|
||||
from ..services.blog_record import BlogRecordService
|
||||
from ..forms import BlogCreateForm, BlogUpdateForm, BlogSearchForm, TagForm
|
||||
import markdown
|
||||
import datetime #<- will be used to set default dates on models
|
||||
|
||||
@view_config(route_name='blog', renderer='../templates/blog.jinja2')
|
||||
def blog(request):
|
||||
# get post id from request
|
||||
blog_id = request.matchdict['id']
|
||||
entry = BlogRecordService.by_id(request, blog_id)
|
||||
|
||||
if not entry:
|
||||
request.session.flash(u"Page non trouvée : %s" % blog_id, 'warning')
|
||||
return HTTPFound(location=request.route_url('home'))
|
||||
|
||||
# insèrer le path de static/img
|
||||
body = entry.body.replace('static/', "%s/static/" % request.application_url)
|
||||
# convertir de markdown en HTML
|
||||
body_html = markdown.markdown(body, extensions=['footnotes'])
|
||||
return_url = "/"
|
||||
|
||||
return {
|
||||
'page_title': entry.title,
|
||||
'entry': entry,
|
||||
'body_html': body_html,
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name='blog_edit', renderer='../templates/blog_edit.jinja2', permission='view')
|
||||
def blog_edit(request):
|
||||
# get post id from request
|
||||
blog_id = request.matchdict['id']
|
||||
url = request.route_url('blog_edit',id=blog_id)
|
||||
|
||||
# get the list of tags
|
||||
tags = BlogRecordService.get_tags(request)
|
||||
|
||||
if blog_id == '0':
|
||||
# create a new post
|
||||
entry = BlogRecord()
|
||||
# set default values
|
||||
form = BlogCreateForm(request.POST, entry)
|
||||
form.tag.choices = [(row.tag, row.tag) for row in tags]
|
||||
page_title = 'Nouvelle page'
|
||||
else:
|
||||
# modify post
|
||||
entry = BlogRecordService.by_id(request, blog_id)
|
||||
if not entry:
|
||||
request.session.flash(u"Page non trouvée : %s" % blog_id, 'warning')
|
||||
return HTTPFound(location=request.route_url('home'))
|
||||
form = BlogUpdateForm(request.POST, entry)
|
||||
form.tag.choices = [(row.tag, row.tag) for row in tags]
|
||||
page_title = 'Modifier : ' + entry.title
|
||||
|
||||
if 'form.submitted' in request.params and form.validate():
|
||||
if blog_id == '0':
|
||||
form.populate_obj(entry)
|
||||
# interdire le car '/' dans le titre à cause du slug
|
||||
entry.title = entry.title.replace('/','.')
|
||||
entry.creator = request.authenticated_userid
|
||||
entry.editor = entry.creator
|
||||
request.dbsession.add(entry)
|
||||
|
||||
return HTTPFound(location=request.route_url('home'))
|
||||
else:
|
||||
del form.id # SECURITY: prevent overwriting of primary key
|
||||
form.populate_obj(entry)
|
||||
# interdire le car '/' dans le titre à cause du slug
|
||||
entry.title = entry.title.replace('/','.')
|
||||
entry.edited = datetime.datetime.now()
|
||||
entry.editor = request.authenticated_userid
|
||||
return HTTPFound(location=request.route_url('blog', id=entry.id, slug=entry.slug))
|
||||
|
||||
if 'form.update_created' in request.params and form.validate():
|
||||
del form.id # SECURITY: prevent overwriting of primary key
|
||||
form.populate_obj(entry)
|
||||
entry.created = datetime.datetime.now()
|
||||
return HTTPFound(location=request.route_url('blog', id=entry.id, slug=entry.slug))
|
||||
|
||||
if 'form.deleted' in request.params:
|
||||
BlogRecordService.delete(request, blog_id)
|
||||
request.session.flash("La page a été supprimée avec succès.", 'success')
|
||||
return HTTPFound(location=request.route_url('home'))
|
||||
|
||||
return {
|
||||
'page_title': page_title,
|
||||
'url': url,
|
||||
'form': form,
|
||||
'blog_id': blog_id,
|
||||
'entry': entry, }
|
||||
|
||||
@view_config(route_name='blog_bytag', renderer='../templates/blog_bytag.jinja2')
|
||||
def blog_bytag(request):
|
||||
# get tag parameters from request
|
||||
tag = request.matchdict['tag']
|
||||
retour = request.matchdict['retour']
|
||||
url_retour = request.route_url(retour)
|
||||
|
||||
# get the posts by tag
|
||||
items = BlogRecordService.by_tag(request, tag)
|
||||
nb_items = len(items)
|
||||
|
||||
return {
|
||||
'page_title': "Billets avec tag : " + tag,
|
||||
'items': items,
|
||||
'nb_items': nb_items,
|
||||
'url_retour': url_retour,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name='blog_search', renderer='../templates/blog_search.jinja2')
|
||||
def blog_search(request):
|
||||
|
||||
criteria = ''
|
||||
# get the list of tags
|
||||
tags = BlogRecordService.get_tags(request)
|
||||
|
||||
form = BlogSearchForm(request.POST)
|
||||
items = []
|
||||
if 'form.submitted' in request.params and form.validate():
|
||||
criteria = request.params['criteria']
|
||||
# si afficher tous les fiches ?
|
||||
items = BlogRecordService.by_criteria(request, criteria)
|
||||
|
||||
return {
|
||||
'page_title': "Rechercher",
|
||||
'form': form,
|
||||
'items': items,
|
||||
'criteria': criteria,
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
@view_config(route_name='tags', renderer='../templates/tags.jinja2', permission='view')
|
||||
def tags(request):
|
||||
|
||||
# get the list of tags
|
||||
tags = BlogRecordService.get_tags(request)
|
||||
|
||||
return {
|
||||
'page_title': 'Tags',
|
||||
'tags': tags,
|
||||
}
|
||||
|
||||
@view_config(route_name='tag_edit', renderer='../templates/tag_edit.jinja2', permission='view')
|
||||
def tag_edit(request):
|
||||
# get tag parameters from request
|
||||
tag_id = request.matchdict['id']
|
||||
url = request.route_url('tag_edit', id=tag_id)
|
||||
|
||||
if tag_id == '0':
|
||||
# create a new tag
|
||||
entry = Tags()
|
||||
form = TagForm(request.POST, entry)
|
||||
page_title = "Nouveau Tag"
|
||||
|
||||
else:
|
||||
# modify post
|
||||
entry = BlogRecordService.get_tags_byId(request, tag_id)
|
||||
if not entry:
|
||||
request.session.flash(u"Tag non trouvé : %s" % tag_id, 'warning')
|
||||
return HTTPFound(location=request.route_url('tags'))
|
||||
form = TagForm(request.POST, entry)
|
||||
page_title = entry.tag
|
||||
|
||||
if 'form.submitted' in request.params and form.validate():
|
||||
if tag_id == '0':
|
||||
form.populate_obj(entry)
|
||||
request.dbsession.add(entry)
|
||||
return HTTPFound(location=request.route_url('tags'))
|
||||
else:
|
||||
del form.id # SECURITY: prevent overwriting of primary key
|
||||
form.populate_obj(entry)
|
||||
return HTTPFound(location=request.route_url('tags'))
|
||||
|
||||
if 'form.deleted' in request.params:
|
||||
BlogRecordService.tag_delete(request, entry.id)
|
||||
request.session.flash("La fiche a été supprimée avec succès.", 'success')
|
||||
return HTTPFound(location=request.route_url('tags'))
|
||||
|
||||
return {
|
||||
'page_title': page_title,
|
||||
'url': url,
|
||||
'form': form,
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
from pyramid.view import (
|
||||
view_config,
|
||||
forbidden_view_config,
|
||||
)
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from pyramid.security import remember, forget
|
||||
from ..services.user import UserService
|
||||
from ..services.blog_record import BlogRecordService
|
||||
from ..forms import UserCreateForm
|
||||
from ..models.user import User
|
||||
|
||||
|
||||
@view_config(route_name='home', renderer='../templates/home.jinja2')
|
||||
def home(request):
|
||||
# get the last created posts
|
||||
last_ten = BlogRecordService.get_last_created(request)
|
||||
# get Chappatte cartoon
|
||||
entry = BlogRecordService.by_id(request, 95)
|
||||
|
||||
return {
|
||||
'page_title': "This is the Way",
|
||||
'last_ten': last_ten,
|
||||
'cartoon_title': entry.title,
|
||||
'cartoon_url': entry.body,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name='apropos',
|
||||
renderer='../templates/apropos.jinja2')
|
||||
def apropos(request):
|
||||
|
||||
return {
|
||||
'page_title': "A propos",
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name='login', renderer='../templates/login.jinja2')
|
||||
@forbidden_view_config(renderer='../templates/login.jinja2')
|
||||
def login(request):
|
||||
login_url = request.route_url('login')
|
||||
|
||||
referrer = request.url
|
||||
if referrer == login_url:
|
||||
referrer = '/' # never use the login form itself as came_from
|
||||
|
||||
came_from = request.params.get('came_from', referrer)
|
||||
username = ''
|
||||
userpwd = ''
|
||||
if 'form.submitted' in request.params:
|
||||
username = request.POST.get('username')
|
||||
userpwd = request.POST.get('password')
|
||||
user = UserService.by_name(request, username)
|
||||
if user and user.verify_password(userpwd):
|
||||
headers = remember(request, username)
|
||||
request.session.flash("Bienvenue %s !" % username, 'success')
|
||||
return HTTPFound(location=came_from, headers=headers)
|
||||
else:
|
||||
headers = forget(request)
|
||||
request.session.flash("Login et mot de passe invalides. La connexion a échoué.", "danger")
|
||||
|
||||
return {
|
||||
'page_title': "",
|
||||
'came_from': came_from,
|
||||
'login_url': login_url,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name='logout', renderer='string')
|
||||
def logout(request):
|
||||
username = request.authenticated_userid
|
||||
if username == None:
|
||||
username = ''
|
||||
|
||||
headers = forget(request)
|
||||
request.session.flash('Au revoir ' + username + ' !', 'success')
|
||||
return HTTPFound(location=request.route_url('home'), headers=headers)
|
||||
|
||||
|
||||
@view_config(route_name='users', renderer='../templates/users.jinja2', permission='manage')
|
||||
def users(request):
|
||||
# get all users
|
||||
users = UserService.all(request)
|
||||
return {
|
||||
'page_title': "Liste des utilisateurs",
|
||||
'users': users
|
||||
}
|
||||
|
||||
@view_config(route_name='user_edit', renderer='ctp_blogr:templates/user_edit.jinja2', permission='view')
|
||||
def user_edit(request):
|
||||
message = ''
|
||||
name = request.matchdict['name']
|
||||
url = request.route_url('user_edit', name=name)
|
||||
if request.authenticated_userid == 'admin':
|
||||
url_retour = request.route_url('users')
|
||||
else:
|
||||
url_retour = request.route_url('home')
|
||||
|
||||
if name == '0':
|
||||
# nouvel utilisateur
|
||||
user = User()
|
||||
form = UserCreateForm(request.POST, user)
|
||||
page_title = "Nouvel utilisateur"
|
||||
else:
|
||||
# lire la fiche du user
|
||||
user = UserService.by_name(request, name)
|
||||
if not user:
|
||||
request.session.flash("Utilisateur non trouvé : %s" % name, 'danger')
|
||||
return HTTPFound(location=url_retour)
|
||||
|
||||
form = UserCreateForm(request.POST, user)
|
||||
page_title = "Modification utilisateur"
|
||||
|
||||
|
||||
if 'form.submitted' in request.params and form.validate():
|
||||
# controle que le password a moins 6 car
|
||||
if len(form.password.data) < 6 :
|
||||
message = "Le mot de passe doit avoir au moins 6 caractères"
|
||||
else:
|
||||
if name == '0':
|
||||
# création user
|
||||
# controler que le nouvel user n'existe pas dans la BD
|
||||
new_user = UserService.by_name(request, form.name.data)
|
||||
if new_user:
|
||||
message = "Utilisateur déjà créé : %s" % form.name.data
|
||||
else:
|
||||
form.populate_obj(user)
|
||||
user.set_password(form.password.data.encode('utf8'))
|
||||
# créer le nouveau
|
||||
request.dbsession.add(user)
|
||||
request.session.flash("La fiche a été créée avec succès.", 'success')
|
||||
return HTTPFound(location=url_retour)
|
||||
|
||||
else:
|
||||
# modification user
|
||||
del form.name # SECURITY: prevent overwriting of primary key
|
||||
form.populate_obj(user)
|
||||
user.set_password(form.password.data.encode('utf8'))
|
||||
request.session.flash("La fiche a été modifiée avec succès.", 'success')
|
||||
return HTTPFound(location=url_retour)
|
||||
|
||||
if 'form.deleted' in request.params:
|
||||
delete(request, user.id)
|
||||
request.session.flash("La fiche a été supprimée avec succès.", 'success')
|
||||
return HTTPFound(location=url_retour)
|
||||
|
||||
return {
|
||||
'page_title': page_title,
|
||||
'message': message,
|
||||
'form': form,
|
||||
'url': url,
|
||||
'url_retour': url_retour,
|
||||
'name': name,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
from pyramid.view import notfound_view_config
|
||||
|
||||
|
||||
@notfound_view_config(renderer='../templates/404.jinja2')
|
||||
def notfound_view(request):
|
||||
request.response.status = 404
|
||||
return {}
|
||||
@@ -0,0 +1,74 @@
|
||||
###
|
||||
# app configuration
|
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
|
||||
###
|
||||
|
||||
[app:main]
|
||||
use = egg:ctp_blogr
|
||||
|
||||
pyramid.reload_templates = true
|
||||
pyramid.debug_authorization = false
|
||||
pyramid.debug_notfound = false
|
||||
pyramid.debug_routematch = false
|
||||
pyramid.default_locale_name = en
|
||||
pyramid.includes =
|
||||
pyramid_debugtoolbar
|
||||
|
||||
sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite
|
||||
|
||||
retry.attempts = 3
|
||||
|
||||
# By default, the toolbar only appears for clients from IP addresses
|
||||
# '127.0.0.1' and '::1'.
|
||||
# debugtoolbar.hosts = 127.0.0.1 ::1
|
||||
|
||||
[pshell]
|
||||
setup = ctp_blogr.pshell.setup
|
||||
|
||||
###
|
||||
# wsgi server configuration
|
||||
###
|
||||
|
||||
[server:main]
|
||||
use = egg:waitress#main
|
||||
listen = localhost:9280
|
||||
|
||||
###
|
||||
# logging configuration
|
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
|
||||
###
|
||||
|
||||
[loggers]
|
||||
keys = root, ctp_blogr, sqlalchemy
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = INFO
|
||||
handlers = console
|
||||
|
||||
[logger_ctp_blogr]
|
||||
level = DEBUG
|
||||
handlers =
|
||||
qualname = ctp_blogr
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
# "level = INFO" logs SQL queries.
|
||||
# "level = DEBUG" logs SQL queries and results.
|
||||
# "level = WARN" logs neither. (Recommended for production systems.)
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
|
||||
@@ -0,0 +1,69 @@
|
||||
###
|
||||
# app configuration
|
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
|
||||
###
|
||||
|
||||
[app:main]
|
||||
use = egg:ctp_blogr
|
||||
|
||||
pyramid.reload_templates = false
|
||||
pyramid.debug_authorization = false
|
||||
pyramid.debug_notfound = false
|
||||
pyramid.debug_routematch = false
|
||||
pyramid.default_locale_name = en
|
||||
|
||||
sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite
|
||||
|
||||
retry.attempts = 3
|
||||
|
||||
[pshell]
|
||||
setup = ctp_blogr.pshell.setup
|
||||
|
||||
###
|
||||
# wsgi server configuration
|
||||
###
|
||||
|
||||
[server:main]
|
||||
use = egg:waitress#main
|
||||
listen = *:9280
|
||||
url_scheme = https
|
||||
|
||||
###
|
||||
# logging configuration
|
||||
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
|
||||
###
|
||||
|
||||
[loggers]
|
||||
keys = root, ctp_blogr, sqlalchemy
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
|
||||
[logger_ctp_blogr]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = ctp_blogr
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
# "level = INFO" logs SQL queries.
|
||||
# "level = DEBUG" logs SQL queries and results.
|
||||
# "level = WARN" logs neither. (Recommended for production systems.)
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths = ctp_blogr
|
||||
python_files = test*.py
|
||||
@@ -0,0 +1,64 @@
|
||||
import os
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
with open(os.path.join(here, 'README.md')) as f:
|
||||
README = f.read()
|
||||
with open(os.path.join(here, 'CHANGES.txt')) as f:
|
||||
CHANGES = f.read()
|
||||
|
||||
requires = [
|
||||
'plaster_pastedeploy',
|
||||
'pyramid',
|
||||
'pyramid_jinja2',
|
||||
'pyramid_debugtoolbar',
|
||||
'waitress',
|
||||
'pyramid_retry',
|
||||
'pyramid_tm',
|
||||
'SQLAlchemy',
|
||||
'transaction',
|
||||
'zope.sqlalchemy',
|
||||
'wtforms', # form library 2.2.1
|
||||
'webhelpers2', # various web building related helpers 2.0
|
||||
'passlib',
|
||||
'markdown',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'WebTest >= 1.3.1', # py3 compat
|
||||
'pytest >= 3.7.4',
|
||||
'pytest-cov',
|
||||
]
|
||||
|
||||
setup(
|
||||
name='ctp_blogr',
|
||||
version='1.0',
|
||||
description='ctp_blogr',
|
||||
long_description=README + '\n\n' + CHANGES,
|
||||
classifiers=[
|
||||
'Programming Language :: Python',
|
||||
'Framework :: Pyramid',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
|
||||
],
|
||||
author='',
|
||||
author_email='',
|
||||
url='',
|
||||
keywords='web pyramid pylons',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
extras_require={
|
||||
'testing': tests_require,
|
||||
},
|
||||
install_requires=requires,
|
||||
entry_points={
|
||||
'paste.app_factory': [
|
||||
'main = ctp_blogr:main',
|
||||
],
|
||||
'console_scripts': [
|
||||
'initialize_ctp_blogr_db=ctp_blogr.scripts.initialize_db:main',
|
||||
],
|
||||
},
|
||||
)
|
||||