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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||