initial upload
This commit is contained in:
3
.coveragerc
Normal file
3
.coveragerc
Normal file
@@ -0,0 +1,3 @@
|
||||
[run]
|
||||
source = pyramid_blogr
|
||||
omit = pyramid_blogr/test*
|
||||
67
.gitignore
vendored
67
.gitignore
vendored
@@ -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
4
CHANGES.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
0.1
|
||||
---
|
||||
|
||||
- Initial version.
|
||||
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal 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
|
||||
44
README.md
44
README.md
@@ -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
43
README.txt
Normal 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
BIN
cao_blogr.sqlite
Normal file
Binary file not shown.
27
cao_blogr/__init__.py
Normal file
27
cao_blogr/__init__.py
Normal 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
58
cao_blogr/alembic/env.py
Normal 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()
|
||||
22
cao_blogr/alembic/script.py.mako
Normal file
22
cao_blogr/alembic/script.py.mako
Normal 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"}
|
||||
43
cao_blogr/alembic/versions/20181223_5899f27f265f.py
Normal file
43
cao_blogr/alembic/versions/20181223_5899f27f265f.py
Normal 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 ###
|
||||
26
cao_blogr/alembic/versions/20220419_bbacde35234d.py
Normal file
26
cao_blogr/alembic/versions/20220419_bbacde35234d.py
Normal 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 ###
|
||||
28
cao_blogr/alembic/versions/20220419_e7889eab89c0.py
Normal file
28
cao_blogr/alembic/versions/20220419_e7889eab89c0.py
Normal 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 ###
|
||||
1
cao_blogr/alembic/versions/README.txt
Normal file
1
cao_blogr/alembic/versions/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
Placeholder for alembic versions
|
||||
26
cao_blogr/forms.py
Normal file
26
cao_blogr/forms.py
Normal 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)])
|
||||
|
||||
78
cao_blogr/models/__init__.py
Normal file
78
cao_blogr/models/__init__.py
Normal 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
|
||||
)
|
||||
33
cao_blogr/models/blog_record.py
Normal file
33
cao_blogr/models/blog_record.py
Normal 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
16
cao_blogr/models/meta.py
Normal 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
34
cao_blogr/models/user.py
Normal 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
13
cao_blogr/pshell.py
Normal 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
12
cao_blogr/routes.py
Normal 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}')
|
||||
1
cao_blogr/scripts/__init__.py
Normal file
1
cao_blogr/scripts/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# package
|
||||
49
cao_blogr/scripts/initialize_db.py
Normal file
49
cao_blogr/scripts/initialize_db.py
Normal 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
15
cao_blogr/security.py
Normal 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
|
||||
|
||||
0
cao_blogr/services/__init__.py
Normal file
0
cao_blogr/services/__init__.py
Normal file
56
cao_blogr/services/blog_record.py
Normal file
56
cao_blogr/services/blog_record.py
Normal 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
|
||||
|
||||
32
cao_blogr/services/user.py
Normal file
32
cao_blogr/services/user.py
Normal 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
|
||||
BIN
cao_blogr/static/favicon.ico
Normal file
BIN
cao_blogr/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
cao_blogr/static/pyramid-16x16.png
Normal file
BIN
cao_blogr/static/pyramid-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
cao_blogr/static/pyramid.png
Normal file
BIN
cao_blogr/static/pyramid.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
132
cao_blogr/static/theme.css
Normal file
132
cao_blogr/static/theme.css
Normal 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;
|
||||
}
|
||||
8
cao_blogr/templates/404.jinja2
Normal file
8
cao_blogr/templates/404.jinja2
Normal 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 %}
|
||||
33
cao_blogr/templates/apropos.jinja2
Normal file
33
cao_blogr/templates/apropos.jinja2
Normal 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 %}
|
||||
31
cao_blogr/templates/blog.jinja2
Normal file
31
cao_blogr/templates/blog.jinja2
Normal 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>
|
||||
|
|
||||
Tag : <strong>{{ entry.tag }}</strong>
|
||||
|
|
||||
Créé le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong>
|
||||
|
|
||||
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 %}
|
||||
51
cao_blogr/templates/blog_edit.jinja2
Normal file
51
cao_blogr/templates/blog_edit.jinja2
Normal 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 %}
|
||||
34
cao_blogr/templates/home.jinja2
Normal file
34
cao_blogr/templates/home.jinja2
Normal 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") }}
|
||||
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">
|
||||
<span class="glyphicon glyphicon-triangle-right"></span> {{ entry.title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-2">
|
||||
<span class="glyphicon glyphicon-triangle-left"></span> {{ entry.tag }}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{{ paginator.pager() |safe }}
|
||||
|
||||
{% else %}
|
||||
|
||||
<p>No blog entries found.</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
104
cao_blogr/templates/layout.jinja2
Normal file
104
cao_blogr/templates/layout.jinja2
Normal 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> {{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">×</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">© 2017 - Phuoc Cao
|
||||
|
||||
{% if request.authenticated_userid == 'admin' %}
|
||||
 | <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>
|
||||
28
cao_blogr/templates/login.jinja2
Normal file
28
cao_blogr/templates/login.jinja2
Normal 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 %}
|
||||
34
cao_blogr/templates/user_add.jinja2
Normal file
34
cao_blogr/templates/user_add.jinja2
Normal 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 %}
|
||||
30
cao_blogr/templates/user_pwd.jinja2
Normal file
30
cao_blogr/templates/user_pwd.jinja2
Normal 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 %}
|
||||
32
cao_blogr/templates/users.jinja2
Normal file
32
cao_blogr/templates/users.jinja2
Normal 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
66
cao_blogr/tests.py
Normal 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)
|
||||
0
cao_blogr/views/__init__.py
Normal file
0
cao_blogr/views/__init__.py
Normal file
68
cao_blogr/views/blog.py
Normal file
68
cao_blogr/views/blog.py
Normal 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
116
cao_blogr/views/default.py
Normal 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,
|
||||
}
|
||||
7
cao_blogr/views/notfound.py
Normal file
7
cao_blogr/views/notfound.py
Normal 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
80
development.ini
Normal 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
74
production.ini
Normal 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
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
testpaths = cao_blogr
|
||||
python_files = test*.py
|
||||
66
setup.py
Normal file
66
setup.py
Normal 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',
|
||||
],
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user