initial upload

This commit is contained in:
2022-04-21 13:49:31 +02:00
parent fc93b5552a
commit e3d4616e62
51 changed files with 1677 additions and 54 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.
# You should customize this list as applicable to your project.
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
node_modules/
*.egg
*.egg-info
*.pyc
*$py.class
*~
.coverage
coverage.xml
build/
dist/
# Compiled Java class files
*.class
# Compiled Python bytecode
*.py[cod]
# Log files
*.log
# Package files
*.jar
# Maven
target/
dist/
# JetBrains IDE
.idea/
# Unit test reports
TEST*.xml
# Generated by MacOS
.tox/
nosetests.xml
env*/
tmp/
Data.fs*
*.sublime-project
*.sublime-workspace
.*.sw?
.sw?
.DS_Store
# Generated by Windows
Thumbs.db
# Applications
*.app
*.exe
*.war
# Large media files
*.mp4
*.tiff
*.avi
*.flv
*.mov
*.wmv
coverage
test

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

@@ -10,12 +10,44 @@ This README would normally document whatever steps are necessary to get your app
### How do I get set up? ###
* Summary of set up
* Configuration
* Dependencies
* Database configuration
* How to run tests
* Deployment instructions
- Change directory into your newly created Pyramid project.
cd cao_blogr
- Create a Python virtual environment.
python3 -m venv env
- Upgrade packaging tools.
env/bin/pip install --upgrade pip setuptools
- Install the project in editable mode with its testing requirements.
env/bin/pip install -e ".[testing]"
- Initialize and upgrade the database using Alembic.
- Generate your first revision.
env/bin/alembic -c development.ini revision --autogenerate -m "init"
- Upgrade to that revision.
env/bin/alembic -c development.ini upgrade head
- Load default data into the database using a script.
env/bin/initialize_cao_blogr_db development.ini
- Run your project's tests.
env/bin/pytest
- Run your project.
env/bin/pserve development.ini
### Contribution guidelines ###

43
README.txt Normal file
View File

@@ -0,0 +1,43 @@
cao_blogr
=========
Getting Started
---------------
- Change directory into your newly created project.
cd cao_blogr
- Create a Python virtual environment.
python3 -m venv env
- Upgrade packaging tools.
env/bin/pip install --upgrade pip setuptools
- Install the project in editable mode with its testing requirements.
env/bin/pip install -e ".[testing]"
- Initialize and upgrade the database using Alembic.
- Generate your first revision.
env/bin/alembic -c development.ini revision --autogenerate -m "init"
- Upgrade to that revision.
env/bin/alembic -c development.ini upgrade head
- Load default data into the database using a script.
env/bin/initialize_cao_blogr_db development.ini
- Run your project's tests.
env/bin/pytest
- Run your project.
env/bin/pserve development.ini

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 pyramid_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,43 @@
"""init
Revision ID: 5899f27f265f
Revises:
Create Date: 2018-12-23 16:39:13.677058
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5899f27f265f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('entries',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.Unicode(length=255), nullable=False),
sa.Column('body', sa.UnicodeText(), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('edited', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id', name=op.f('pk_entries')),
sa.UniqueConstraint('title', name=op.f('uq_entries_title'))
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.Unicode(length=255), nullable=False),
sa.Column('password', sa.Unicode(length=255), nullable=False),
sa.Column('last_logged', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id', name=op.f('pk_users')),
sa.UniqueConstraint('name', name=op.f('uq_users_name'))
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('users')
op.drop_table('entries')
# ### end Alembic commands ###

View File

@@ -0,0 +1,26 @@
"""init
Revision ID: bbacde35234d
Revises: e7889eab89c0
Create Date: 2022-04-19 17:09:50.728285
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bbacde35234d'
down_revision = 'e7889eab89c0'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('entries', sa.Column('body_html', sa.UnicodeText(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('entries', 'body_html')
# ### end Alembic commands ###

View File

@@ -0,0 +1,28 @@
"""init
Revision ID: e7889eab89c0
Revises: 5899f27f265f
Create Date: 2022-04-19 16:21:57.531003
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e7889eab89c0'
down_revision = '5899f27f265f'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('entries', sa.Column('tag', sa.Unicode(), nullable=True))
op.add_column('entries', sa.Column('topic', sa.Unicode(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('entries', 'topic')
op.drop_column('entries', 'tag')
# ### end Alembic commands ###

View File

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

26
cao_blogr/forms.py Normal file
View File

@@ -0,0 +1,26 @@
from wtforms import Form, StringField, TextAreaField, 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])
topic = StringField('Topic', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
tag = StringField('Tag', validators=[InputRequired(), Length(min=1, max=20)],
filters=[strip_filter])
class BlogUpdateForm(BlogCreateForm):
id = IntegerField(widget=HiddenInput())
class UserCreateForm(Form):
username = StringField('Nom', [validators.required(), validators.Length(min=1, max=255)],
filters=[strip_filter])
password = PasswordField('Mot de passe', validators.required(), [validators.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,33 @@
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=u'')
body_html = Column(UnicodeText, default=u'')
tag = Column(Unicode, default=u'pyramid')
topic = Column(Unicode, default=u'blog')
created = Column(DateTime, default=datetime.datetime.utcnow)
edited = Column(DateTime, default=datetime.datetime.utcnow)
@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())

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

12
cao_blogr/routes.py Normal file
View File

@@ -0,0 +1,12 @@
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('page_search', '/page_search')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
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,56 @@
import sqlalchemy as sa
import datetime #<- will be used to set default dates on models
from paginate_sqlalchemy import SqlalchemyOrmPage #<- provides pagination
from ..models.blog_record import BlogRecord
from markdown2 import Markdown
class BlogRecordService(object):
@classmethod
def all(cls, request):
query = request.dbsession.query(BlogRecord)
return query.order_by(sa.desc(BlogRecord.created))
@classmethod
def by_id(cls, request, _id):
query = request.dbsession.query(BlogRecord)
return query.get(_id)
@classmethod
def get_paginator(cls, request, page=1):
query = request.dbsession.query(BlogRecord)
query = query.order_by(sa.desc(BlogRecord.created))
query_params = request.GET.mixed()
def url_maker(link_page):
# replace page param with values generated by paginator
query_params['page'] = link_page
return request.current_route_url(_query=query_params)
return SqlalchemyOrmPage(query, page, items_per_page=5,
url_maker=url_maker)
@classmethod
def proc_after_create(cls, request, _id):
entry = request.dbsession.query(BlogRecord).get(_id)
# set default values
if entry.tag == '':
entry.tag = 'pyramid'
if entry.topic == '':
entry.topic = 'blog'
# convertir mardown en HTML
markdowner = Markdown()
entry.body_html = markdowner.convert(entry.body)
return
@classmethod
def proc_after_update(cls, request, _id):
entry = request.dbsession.query(BlogRecord).get(_id)
entry.edited = datetime.datetime.now()
# convertir mardown en HTML
markdowner = Markdown()
entry.body_html = markdowner.convert(entry.body)
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

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

@@ -0,0 +1,132 @@
/* 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;
}
.open .dropdown-toggle {
color: #fff;
background-color: #555 !important;
}
.dropdown-menu li a {
color: #000 !important;
}
.dropdown-menu li a:hover {
background-color: red !important;
}
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;
}

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,31 @@
{% 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>{{ entry.body_html | safe }}</p>
<hr/>
{% if request.authenticated_userid %}
<p>
Topic : <strong>{{ entry.topic }}</strong>
&nbsp;|&nbsp;
Tag : <strong>{{ entry.tag }}</strong>
&nbsp;|&nbsp;
Créé le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong>
&nbsp;|&nbsp;
Modifié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong>
</p>
{% else %}
<p>
Créé : <strong title="{{ entry.created }}">{{ entry.created_in_words }}</strong>
</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% 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 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 for="body">{{ form.body.label }}</label>
{{ form.body(class_='form-control', cols="35", rows="20") }}
</div>
{% for error in form.topic.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label for="title">{{ form.topic.label }}</label>
{{ form.topic(class_='form-control') }}
</div>
{% for error in form.tag.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label for="title">{{ form.tag.label }}</label>
{{ form.tag(class_='form-control') }}
</div>
<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 action == 'edit' %}
<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,34 @@
{% extends "layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<p><a href="{{ request.route_url('blog_edit', id='0') }}">
[Nouveau post]</a>
</p>
{% endif%}
{% if paginator.items %}
{% for entry in paginator.items %}
<div class="col-xs-10">
{{ entry.created.strftime("%d-%m-%Y") }}&nbsp;&nbsp;
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
<span class="glyphicon glyphicon-triangle-right"></span>&nbsp;{{ entry.title }}
</a>
</div>
<div class="col-xs-2">
<span class="glyphicon glyphicon-triangle-left"></span>&nbsp;{{ entry.tag }}
</div>
{% endfor %}
{{ paginator.pager() |safe }}
{% else %}
<p>No blog entries found.</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,104 @@
<!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="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Custom styles for this scaffold -->
<link href="{{request.static_url('cao_blogr:static/theme.css')}}" rel="stylesheet">
<!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
<script src="//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js" integrity="sha384-ZoaMbDF+4LeFxg6WdScQ9nnR1QC2MIRxA1O9KWEXQwns1G8UNyIEZIQidzb0T1fo" crossorigin="anonymous"></script>
<![endif]-->
</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') }}">CTP Blog</a>
</div>
<div class="collapse navbar-collapse" id="myNavbar">
<ul class="nav navbar-nav navbar-right">
<li><a href="#band">TAGS</a></li>
<li><a href="{{ request.route_url('apropos')}}">A PROPOS</a></li>
<li><a href="{{ request.route_url('page_search') }}"><span class="glyphicon glyphicon-search"></span></a></li>
<!-- si anonyme, lien pour se connecter -->
{% if request.authenticated_userid %}
<li><a href="{{request.route_url('logout')}}">
<span class="glyphicon glyphicon-log-out"></span>&nbsp{{request.authenticated_userid}}</a></li>
{% else %}
<li><a href="{{request.route_url('login')}}"><span class="glyphicon glyphicon-log-in"></span></a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Container (The Page Template Section) -->
<div class="container">
<div class="row">
<!-- 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>
</div>
<!-- Footer -->
<footer class="text-center">
<div class="row">
<p class="text-center">&copy; 2017&nbsp;-&nbsp;Phuoc Cao
{% if request.authenticated_userid == 'admin' %}
&nbsp|&nbsp<a href="{{request.route_url('users')}}" alt = "Utilisateurs">
<span class="glyphicon glyphicon-user"></span></a>
{% endif %}
</p>
</div>
</footer>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="//code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/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="{{request.route_url('login')}}" 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,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 for="title">{{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 for="body">{{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">
</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 %}

66
cao_blogr/tests.py Normal file
View File

@@ -0,0 +1,66 @@
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 setUp(self):
super(TestMyViewSuccessCondition, self).setUp()
self.init_database()
from .models import MyModel
model = MyModel(name='one', value=55)
self.session.add(model)
def test_passing_view(self):
from .views.default import my_view
info = my_view(dummy_request(self.session))
self.assertEqual(info['one'].name, 'one')
self.assertEqual(info['project'], 'cao_blogr')
class TestMyViewFailureCondition(BaseTest):
def test_failing_view(self):
from .views.default import my_view
info = my_view(dummy_request(self.session))
self.assertEqual(info.status_int, 500)

View File

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

@@ -0,0 +1,68 @@
from pyramid.view import view_config
from pyramid.httpexceptions import HTTPNotFound, HTTPFound
from ..models.blog_record import BlogRecord
from ..services.blog_record import BlogRecordService
from ..forms import BlogCreateForm, BlogUpdateForm
@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)
# just created ? convert body to html
if entry.body_html == '':
BlogRecordService.proc_after_create(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'))
return {
'page_title': entry.title,
'entry': entry
}
@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)
if blog_id == '0':
# create a new post
entry = BlogRecord()
form = BlogCreateForm(request.POST)
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)
if 'form.submitted' in request.params and form.validate():
if blog_id == '0':
form.populate_obj(entry)
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)
# after update procedure
BlogRecordService.proc_after_update(request, blog_id)
return HTTPFound(location=request.route_url('blog', id=entry.id, slug=entry.slug))
return {
'page_title': entry.title,
'url': url,
'form': form,
}

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

@@ -0,0 +1,116 @@
from pyramid.view import 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='cao_blogr:templates/home.jinja2')
def home(request):
page = int(request.params.get('page', 1))
paginator = BlogRecordService.get_paginator(request, page)
return {
'page_title': "Bienvenue sur mon blog",
'paginator': paginator
}
@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')
def login(request):
username = request.POST.get('username')
if username:
user = UserService.by_name(request, username)
if user and user.verify_password(request.POST.get('password')):
headers = remember(request, user.name)
request.session.flash("Bienvenue %s !" % username, 'success')
return HTTPFound(location=request.route_url('home'), headers=headers)
else:
headers = forget(request)
request.session.flash("Login et mot de passe invalides. La connexion a échoué.", "danger")
return {
'page_title': "",
}
@view_config(route_name='logout', renderer='string')
def logout(request):
headers = forget(request)
request.session.flash('Vous avez bien été déconnecté.', '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.txt')) 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_tm',
'SQLAlchemy',
'transaction',
'zope.sqlalchemy',
'wtforms==2.2.1', # form library
'webhelpers2==2.0', # various web building related helpers
'paginate==0.5.6', # pagination helpers
'paginate_sqlalchemy==0.3.0',
'passlib',
]
tests_require = [
'WebTest >= 1.3.1', # py3 compat
'pytest >= 3.7.4',
'pytest-cov',
]
setup(
name='cao_blogr',
version='0.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',
],
},
)