ititial upload

This commit is contained in:
2023-06-25 16:56:39 +02:00
parent 1bbc913a20
commit a4628bd5bd
51 changed files with 1861 additions and 67 deletions

3
.coveragerc Normal file
View File

@@ -0,0 +1,3 @@
[run]
source = pyramid_blogr
omit = pyramid_blogr/test*

67
.gitignore vendored
View File

@@ -1,50 +1,21 @@
# These are some examples of commonly ignored file patterns. *.egg
# You should customize this list as applicable to your project. *.egg-info
# Learn more about .gitignore: *.pyc
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore *$py.class
*~
# Node artifact files .coverage
node_modules/ coverage.xml
build/
dist/ dist/
.tox/
# Compiled Java class files nosetests.xml
*.class env*/
tmp/
# Compiled Python bytecode Data.fs*
*.py[cod] *.sublime-project
*.sublime-workspace
# Log files .*.sw?
*.log .sw?
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.DS_Store .DS_Store
coverage
# Generated by Windows test
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv

4
CHANGES.txt Normal file
View File

@@ -0,0 +1,4 @@
0.1
---
- Initial version.

2
MANIFEST.in Normal file
View File

@@ -0,0 +1,2 @@
include *.txt *.ini *.cfg *.rst
recursive-include cao_blogr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2

View File

@@ -1,29 +1,29 @@
# README # # README #
This README would normally document whatever steps are necessary to get your application up and running. **cao_blogr** est une application pour créer un blog simple. Elle est inspirée du tutorial [**pyramid_blogr**](https://docs.pylonsproject.org/projects/pyramid-blogr/en/latest/index.html).
### What is this repository for? ### ## Fonctionnalités ##
* Quick summary Bien que **cao_blogr** soit une application minimale et simple, elle possède toutes les fonctions nécessaire pour gérer un blog :
* Version
* [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo)
### How do I get set up? ### - 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
* Summary of set up ## Add-on requis ##
* Configuration
* Dependencies
* Database configuration
* How to run tests
* Deployment instructions
### Contribution guidelines ### - Python 3.7.1
- Pyramid 1.10
- SQLite 3.35.5
- pyramid-jinja2 2.7 : view templating
- wtforms 2.2.1 : form library
- webhelpers2 2.0 : various web building related helpers
- Markdown 3.4.1 :
* Writing tests
* Code review
* Other guidelines
### Who do I talk to? ### ### 1.0 (22.01.2023)
* Repo owner or admin - version initiale
* Other community or team contact

BIN
cao_blogr.sqlite Normal file

Binary file not shown.

27
cao_blogr/__init__.py Normal file
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('mGcAJn2HmNH6Hc')
authentication_policy = AuthTktAuthenticationPolicy('wMWvAWMZnp6Lch',
callback=groupfinder, hashalg='sha512', timeout=36000)
authorization_policy = ACLAuthorizationPolicy()
with Configurator(settings=settings,
root_factory='cao_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()

58
cao_blogr/alembic/env.py Normal file
View File

@@ -0,0 +1,58 @@
"""Pyramid bootstrap environment. """
from alembic import context
from pyramid.paster import get_appsettings, setup_logging
from sqlalchemy import engine_from_config
from cao_blogr.models.meta import Base
config = context.config
setup_logging(config.config_file_name)
settings = get_appsettings(config.config_file_name)
target_metadata = Base.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
context.configure(url=settings['sqlalchemy.url'])
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(settings, prefix='sqlalchemy.')
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,26 @@
"""init
Revision ID: a632e375e7dc
Revises:
Create Date: 2023-01-21 11:25:48.517435
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a632e375e7dc'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('entries', 'author')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('entries', sa.Column('author', sa.VARCHAR(length=50), nullable=True))
# ### end Alembic commands ###

View File

@@ -0,0 +1 @@
Placeholder for alembic versions

34
cao_blogr/forms.py Normal file
View File

@@ -0,0 +1,34 @@
from wtforms import Form, StringField, TextAreaField, SelectField, validators
from wtforms import IntegerField, PasswordField
from wtforms.validators import InputRequired, Length
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('Recherche', 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):
username = StringField('Nom', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
password = PasswordField('Mot de passe', validators=[InputRequired(), Length(min=6)])

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('cao_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
)

View File

@@ -0,0 +1,40 @@
import datetime #<- will be used to set default dates on models
from cao_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
cao_blogr/models/meta.py Normal file
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
cao_blogr/models/user.py Normal file
View File

@@ -0,0 +1,34 @@
import datetime #<- will be used to set default dates on models
from cao_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
cao_blogr/pshell.py Normal file
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

14
cao_blogr/routes.py Normal file
View File

@@ -0,0 +1,14 @@
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_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_add', '/user_add/{name}')
config.add_route('user_pwd', '/user_pwd/{name}')

View File

@@ -0,0 +1 @@
# package

View File

@@ -0,0 +1,49 @@
import argparse
import sys
from pyramid.paster import bootstrap, setup_logging
from sqlalchemy.exc import OperationalError
from .. import models
def setup_models(dbsession):
"""
Add or update models / fixtures in the database.
"""
model = models.user.User(name=u'admin', password=u'pcao.7513')
dbsession.add(model)
def parse_args(argv):
parser = argparse.ArgumentParser()
parser.add_argument(
'config_uri',
help='Configuration file, e.g., development.ini',
)
return parser.parse_args(argv[1:])
def main(argv=sys.argv):
args = parse_args(argv)
setup_logging(args.config_uri)
env = bootstrap(args.config_uri)
try:
with env['request'].tm:
dbsession = env['request'].dbsession
setup_models(dbsession)
except OperationalError:
print('''
Pyramid is having a problem using your SQL database. The problem
might be caused by one of the following things:
1. You may need to initialize your database tables with `alembic`.
Check your README.txt for description and try to run it.
2. Your database server may not be running. Check that the
database server referred to by the "sqlalchemy.url" setting in
your "development.ini" file is running.
''')

15
cao_blogr/security.py Normal file
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

View File

@@ -0,0 +1,57 @@
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)
if request.authenticated_userid == None:
# if user is anonym, display only published posts
query = query.filter(BlogRecord.status == 'publié')
query = query.filter(or_(BlogRecord.title.like(search),
BlogRecord.body.like(search))).all()
return query
@classmethod
def by_id(cls, request, _id):
query = request.dbsession.query(BlogRecord)
return query.get(_id)
@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(10).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

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: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

143
cao_blogr/static/theme.css Normal file
View File

@@ -0,0 +1,143 @@
/* Theme inspired from Bootstrap Theme "The Band" */
/* https://www.w3schools.com/bootstrap/bootstrap_theme_band.asp */
body {
font: 400 19px/1.8 Georgia, serif;
color: #777;
}
h3, h4 {
margin: 10px 0 30px 0;
letter-spacing: 10px;
font-size: 20px;
color: #111;
}
.container {
padding: 80px 120px;
}
.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 */
}
}
.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;
}
.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;
}

View File

@@ -0,0 +1,8 @@
{% extends "layout.jinja2" %}
{% block content %}
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
</div>
{% endblock content %}

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 %}

View File

@@ -0,0 +1,34 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<p>
<a href="{{ request.route_url('home') }}">[ Retour ]</a>
<a href="{{ request.route_url('blog_edit', id=entry.id) }}">[ Modifier ]</a>
</p>
{% endif %}
<hr/>
<p>{{ body_html | safe }}</p>
<hr/>
<p>
Auteur : <strong>{{ entry.author }}</strong><br>
Publié le : <strong>{{ entry.created.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 %}

View File

@@ -0,0 +1,80 @@
{% extends "cao_blogr:templates/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', 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><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 %}

View File

@@ -0,0 +1,51 @@
{% extends "cao_blogr:templates/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 />
<br />
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<p><a href="{{ request.route_url('blog_edit', id='0') }}" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-plus"></span> Nouveau</a>
</p>
{% endif%}
<table id="posts_list" class="table table-condensed">
{% for entry in last_ten %}
<tr>
<td>{{ entry.created.strftime("%d.%m.%Y") }}</td>
<td>
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
</td>
<td>{{ entry.tag }}</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>
<!-- formulaire de recherche -->
<hr>
<br>
<br>
<div class="well">
<form id="search-form" role="form" action="/blog_search" method="post">
<div class="form-group">
<label for="criteria">{{ form.criteria.label }}</label>
<div class="input-group">
{{ form.criteria(class_='form-control') }}
<span class="input-group-btn">
<button 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>
</form>
</div>
{% endblock %}

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('cao_blogr:static/pyramid-16x16.png')}}">
<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('cao_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') }}">CAO Blogr</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>
{% 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>
{% if request.path == '/' %}
{# -- display carousel -- #}
{% block carousel %}
{% endblock carousel %}
{% endif %}
<!-- Container (The Page Template Section) -->
<div class="container">
<!-- 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>

View File

@@ -0,0 +1,28 @@
{% 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>
<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" value="Se connecter" class="btn btn-primary">
</div>
</form>
</div>
</div>
<br />
<br />
<br />
<br />
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends "cao_blogr:templates/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 %}

View File

@@ -0,0 +1,26 @@
{% extends "cao_blogr:templates/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 %}

View File

@@ -0,0 +1,34 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{request.route_url('user_add', name=name)}}" method="post" class="form">
{% for error in form.username.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="username">{{form.username.label}}</label>
{{form.username(class_='form-control')}}
</div>
{% for error in form.password.errors %}
<div class="error">{{error}}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="password">{{form.password.label}}</label>
{{form.password(class_='form-control')}}
</div>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('users') }}"><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>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,30 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ request.route_url('user_pwd', name=entry.name) }}" method="post" class="form">
<div class="form-group">
<label for="password">Nouveau mot de passe</label></label>
<input type="password" name="new_password" class="form-control" placeholder="Optionel">
</div>
<div class="form-group">
<div class="form-control-static text-success">
<strong>Dernière connexion</strong> :
{{ entry.last_logged.strftime("%d-%m-%Y - %H:%M") }}</div>
</div>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('users') }}"><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 name != 'new' %}
<button class="btn btn-warning" type="submit" name="form.deleted">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
{% endblock %}

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_add', name='new') }}" 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_pwd', name=entry.name) }}">
{{ entry.name }}
</a>
</td>
<td>{{ entry.last_logged.strftime("%d-%m-%Y - %H:%M") }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

56
cao_blogr/tests.py Normal file
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

160
cao_blogr/views/blog.py Normal file
View File

@@ -0,0 +1,160 @@
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='cao_blogr: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 {
'page_title': entry.title,
'entry': entry,
'body_html': body_html,
}
@view_config(route_name='blog_edit', renderer='cao_blogr: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.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_search',
renderer='cao_blogr:templates/blog_search.jinja2')
def blog_search(request):
criteria = ''
items = []
form = BlogSearchForm(request.POST)
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,
}
@view_config(route_name='tags', renderer='cao_blogr:templates/tags.jinja2', permission='view')
def tags(request):
# get the list of tags of this topic
tags = BlogRecordService.get_tags(request)
return {
'page_title': 'Tags',
'tags': tags,
}
@view_config(route_name='tag_edit', renderer='cao_blogr: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,
}

133
cao_blogr/views/default.py Normal file
View File

@@ -0,0 +1,133 @@
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, BlogSearchForm
from ..models.user import User
@view_config(route_name='home',
renderer='cao_blogr:templates/home.jinja2')
def home(request):
# get the last created posts
last_ten = BlogRecordService.get_last_created(request)
criteria = ''
form = BlogSearchForm(request.POST)
return {
'page_title': "Bienvenue sur mon blog",
'last_ten': last_ten,
'form': form,
'criteria': criteria,
}
@view_config(route_name='apropos',
renderer='cao_blogr:templates/apropos.jinja2')
def apropos(request):
return {
'page_title': "A propos",
}
@view_config(route_name='login', renderer='cao_blogr:templates/login.jinja2')
@forbidden_view_config(renderer='cao_blogr:templates/login.jinja2')
def login(request):
username = ''
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 = request.POST.get('username')
userpwd = request.POST.get('password')
if username:
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
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='cao_blogr: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_add', renderer='cao_blogr:templates/user_add.jinja2', permission='manage')
def user_add(request):
name = request.matchdict['name']
# nouveau
form = UserCreateForm(request.POST)
if 'form.submitted' in request.params and form.validate():
# créer nouveau
new_user = User(name=form.username.data)
new_user.set_password(form.password.data.encode('utf8'))
request.dbsession.add(new_user)
return HTTPFound(location=request.route_url('users'))
return {
'page_title': 'Nouvel utilsateur',
'form': form,
'name': name,
}
@view_config(route_name='user_pwd', renderer='cao_blogr:templates/user_pwd.jinja2', permission='manage')
def user_pwd(request):
# reset password or delete user
name = request.matchdict['name']
# lire la fiche du membre
entry = UserService.by_name(request, name)
if not entry:
request.session.flash(u"Utilisateur non trouvé : %s" % name, 'warning')
return HTTPFound(location=request.route_url('users'))
if 'form.submitted' in request.params:
mdp = request.params["new_password"]
entry.set_password(mdp.encode('utf8'))
return HTTPFound(location=request.route_url('users'))
if 'form.deleted' in request.params:
UserService.delete(request, entry.id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('users'))
return {
'page_title': "Utilisateur : %s" %(entry.name),
'entry': entry,
}

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 {}

80
development.ini Normal file
View File

@@ -0,0 +1,80 @@
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:cao_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/cao_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 = cao_blogr.pshell.setup
###
# wsgi server configuration
###
[alembic]
# path to migration scripts
script_location = cao_blogr/alembic
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
# file_template = %%(rev)s_%%(slug)s
[server:main]
use = egg:waitress#main
listen = localhost:6543
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, cao_blogr, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_cao_blogr]
level = DEBUG
handlers =
qualname = cao_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

74
production.ini Normal file
View File

@@ -0,0 +1,74 @@
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###
[app:main]
use = egg:cao_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/cao_blogr.sqlite
retry.attempts = 3
[pshell]
setup = cao_blogr.pshell.setup
###
# wsgi server configuration
###
[alembic]
# path to migration scripts
script_location = cao_blogr/alembic
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
# file_template = %%(rev)s_%%(slug)s
[server:main]
use = egg:waitress#main
listen = *:6543
###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###
[loggers]
keys = root, cao_blogr, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
[logger_cao_blogr]
level = WARN
handlers =
qualname = cao_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
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
testpaths = cao_blogr
python_files = test*.py

1
rtd.txt Normal file
View File

@@ -0,0 +1 @@
pylons-sphinx-themes

66
setup.py Normal file
View File

@@ -0,0 +1,66 @@
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',
'alembic',
'pyramid_retry',
'pyramid_layout',
'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='cao_blogr',
version='1.0',
description='cao_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 = cao_blogr:main',
],
'console_scripts': [
'initialize_cao_blogr_db=cao_blogr.scripts.initialize_db:main',
],
},
)