Inital commit since swicthing to python 3.14

This commit is contained in:
2026-10-05 14:55:19 +02:00
commit 411ca8d2e8
71 changed files with 1829 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
[run]
source = pyramid_blogr
omit = pyramid_blogr/test*
+22
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
0.1
---
- Initial version.
+2
View File
@@ -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
+29
View File
@@ -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
BIN
View File
Binary file not shown.
+27
View File
@@ -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()
+66
View File
@@ -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()])
+78
View File
@@ -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
)
+40
View File
@@ -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))
+16
View File
@@ -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)
+34
View File
@@ -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
+13
View File
@@ -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
+15
View File
@@ -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}')
+1
View File
@@ -0,0 +1 @@
# package
+15
View File
@@ -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
View File
+65
View File
@@ -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
+32
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+163
View File
@@ -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;
}
+8
View File
@@ -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 %}
+33
View File
@@ -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 %}
+33
View File
@@ -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 %}
+26
View File
@@ -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>&nbsp;</td>
{% endif%}
</tr>
{% else %}
<p class="text-danger">Aucun post trouvé !</p>
{% endfor %}
</table>
<p class="text-center">[ {{nb_items}} billets ]</p>
{% endblock %}
+82
View File
@@ -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">&times;</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 %}
+57
View File
@@ -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">
|&nbsp;
{% for tag in tags %}
<a href="{{ request.route_url('blog_bytag', tag=tag.tag, retour='blog_search') }}">{{ tag.tag }}</a>&nbsp;|
{% endfor %}
</p>
{% endblock %}
+43
View File
@@ -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>&nbsp;</td>
{% endif%}
</tr>
{% else %}
<p class="text-danger">Aucun post trouvé</p>
{% endfor %}
</table>
{% endblock %}
+109
View File
@@ -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>&nbsp;&nbsp;Utilisateurs</a></li>
{% else %}
<li><a href="{{request.route_url('user_edit', name=request.authenticated_userid)}}">
<span class="glyphicon glyphicon-user"></span>&nbsp;&nbsp;Modifier le mot de passe</a>
</li>
{% endif %}
<li><a href="{{ request.route_url('tags') }}"><span class="glyphicon glyphicon-tag"></span>&nbsp;&nbsp;Tags</a></li>
<li><a href="{{ request.route_url('logout') }}"><span class="glyphicon glyphicon-off"></span>&nbsp;&nbsp;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">&times;</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">
&copy; 2017&nbsp;-&nbsp;Phuoc Cao
&nbsp|&nbsp<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>
+30
View File
@@ -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 %}
+55
View File
@@ -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">&times;</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 %}
+26
View File
@@ -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 %}
+55
View File
@@ -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 %}
+32
View File
@@ -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 %}
+56
View File
@@ -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')
View File
+188
View File
@@ -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,
}
+153
View File
@@ -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,
}
+7
View File
@@ -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 {}
+74
View File
@@ -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
+69
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
[pytest]
testpaths = ctp_blogr
python_files = test*.py
+1
View File
@@ -0,0 +1 @@
pylons-sphinx-themes
+64
View File
@@ -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',
],
},
)