commit 411ca8d2e859abe65d5be3ac5f1f288822ec2fab Author: Phuoc Cao Date: Mon Oct 5 14:55:19 2026 +0200 Inital commit since swicthing to python 3.14 diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..9ece22e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = pyramid_blogr +omit = pyramid_blogr/test* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de985c --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.egg +*.egg-info +*.pyc +*$py.class +*~ +*.sqlite +.coverage +coverage.xml +build/ +dist/ +.tox/ +nosetests.xml +env*/ +tmp/ +Data.fs* +*.sublime-project +*.sublime-workspace +.*.sw? +.sw? +.DS_Store +coverage +test diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..c9f9b0f --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,4 @@ +0.1 +--- + +- Initial version. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8ebe596 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include ctp_blogr *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/README.md b/README.md new file mode 100644 index 0000000..450b740 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# README # + +**ctp_blogr** est une application pour créer un blog simple. + +## Fonctionnalités ## + +Bien que **ctp_blogr** soit une application minimale et simple, elle possède toutes les fonctions nécessaire pour gérer un blog : + +- Gestion des utilisateurs du blog (admin ou rédacteur) +- Authentification et autorisation des utilisateurs +- Gestion des posts du blog avec des statuts : *publié, brouillon ou privé* +- Gestion des tags (un post peut avoir un tag) +- Les posts sont rédigé avec le langage *Markdown* +- Possibilité de faire des recherches sur le titre et le corps des posts + +## Add-on requis ## + +- Python 3.9.6 +- Pyramid 2.0.2 +- SQLite 3.43.2 +- pyramid-jinja2 2.7 : view templating +- wtforms 3.2.1 : form library +- webhelpers2 2.0 : various web building related helpers +- Markdown 3.4.1 : + + +### 1.0 (22.01.2023) + +- version initiale \ No newline at end of file diff --git a/ctp_blogr.sqlite b/ctp_blogr.sqlite new file mode 100644 index 0000000..8fa13a8 Binary files /dev/null and b/ctp_blogr.sqlite differ diff --git a/ctp_blogr/__init__.py b/ctp_blogr/__init__.py new file mode 100644 index 0000000..20ebb3a --- /dev/null +++ b/ctp_blogr/__init__.py @@ -0,0 +1,27 @@ +from pyramid.config import Configurator +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.session import SignedCookieSessionFactory + +from .services.user import groupfinder + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + # session factory + my_session_factory = SignedCookieSessionFactory('hZug2zPt7hT2MZ') + + authentication_policy = AuthTktAuthenticationPolicy('J2wv322aL5DTn2', + callback=groupfinder, hashalg='sha512', timeout=36000) + authorization_policy = ACLAuthorizationPolicy() + with Configurator(settings=settings, + root_factory='ctp_blogr.security.RootFactory', + authentication_policy=authentication_policy, + authorization_policy=authorization_policy) as config: + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.set_session_factory(my_session_factory) + config.scan() + return config.make_wsgi_app() diff --git a/ctp_blogr/forms.py b/ctp_blogr/forms.py new file mode 100644 index 0000000..03d0480 --- /dev/null +++ b/ctp_blogr/forms.py @@ -0,0 +1,66 @@ +from wtforms import Form, StringField, TextAreaField, SelectField, DecimalField +from wtforms import IntegerField, PasswordField +from wtforms.validators import InputRequired, Length, EqualTo +from wtforms.widgets import HiddenInput + +strip_filter = lambda x: x.strip() if x else None + +class BlogCreateForm(Form): + title = StringField('Titre', validators=[InputRequired(), Length(min=1, max=255)], + filters=[strip_filter]) + body = TextAreaField('Corps du texte', validators=[InputRequired(), Length(min=1)], + filters=[strip_filter]) + tag = SelectField('Tag') + status = SelectField('Statut', choices=[('brouillon','Brouillon'),('privé','Privé'),('publié','Publié')]) + +class BlogUpdateForm(BlogCreateForm): + id = IntegerField(widget=HiddenInput()) + +class BlogSearchForm(Form): + criteria = StringField('Critère', validators=[InputRequired(), Length(min=3, max=45)], + filters=[strip_filter]) + +class TagForm(Form): + id = IntegerField(widget=HiddenInput()) + tag = StringField('Tag', validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter]) + + +class UserCreateForm(Form): + id = IntegerField(widget=HiddenInput()) + name = StringField('Nom', validators=[InputRequired(), Length(min=1, max=255)], + filters=[strip_filter]) + password = PasswordField('Mot de passe') + confirm = PasswordField('Confirmer', validators=[EqualTo('password', message='Les 2 Passwords doivent être identiques')]) + +class HistoForm(Form): + no_id = IntegerField(widget=HiddenInput()) + mvt_cash = DecimalField('Montant à ajouter au portefeuille', places=2, validators=[InputRequired()]) + +class AllocationForm(Form): + no_cat = IntegerField(widget=HiddenInput()) + classe = StringField("Classe d'actif", validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter]) + pc_cible = IntegerField(validators=[InputRequired()]) + type = SelectField('Type', choices=[('ACTION','ACTION'),('AUTRE','AUTRE')]) + ordre = IntegerField(validators=[InputRequired()]) + bg_color = SelectField('Couleur de fond', choices=[('info','BLEU'),('danger','ROUGE'),('warning','ORANGE'),('success','VERT')]) + +class ActifForm(Form): + no_id = IntegerField(widget=HiddenInput()) + classe = SelectField('Classe') + symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter]) + libelle = StringField('Nom', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter]) + nombre = IntegerField(validators=[InputRequired()]) + pru = DecimalField('PRU', places=3, validators=[InputRequired()]) + ter = DecimalField('TER', places=2, validators=[InputRequired()]) + pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()]) + website = StringField('Web site', validators=[Length(min=1, max=100)], filters=[strip_filter]) + +class Actif2Form(Form): + no_id = IntegerField(widget=HiddenInput()) + classe = SelectField('Classe') + symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter]) + libelle = StringField('Libellé', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter]) + pru = DecimalField('Total investi', places=3, validators=[InputRequired()]) + cours = DecimalField('Total valeur', places=3, validators=[InputRequired()]) + pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()]) + diff --git a/ctp_blogr/models/__init__.py b/ctp_blogr/models/__init__.py new file mode 100644 index 0000000..d82b4c0 --- /dev/null +++ b/ctp_blogr/models/__init__.py @@ -0,0 +1,78 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .user import User +from .blog_record import BlogRecord + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('ctp_blogr.models')``. + + """ + settings = config.get_settings() + settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager' + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + # use pyramid_retry to retry a request when transient exceptions occur + config.include('pyramid_retry') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/ctp_blogr/models/blog_record.py b/ctp_blogr/models/blog_record.py new file mode 100644 index 0000000..342a9f6 --- /dev/null +++ b/ctp_blogr/models/blog_record.py @@ -0,0 +1,40 @@ +import datetime #<- will be used to set default dates on models +from ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit +from sqlalchemy import ( + Column, + Integer, + Unicode, #<- will provide Unicode field + UnicodeText, #<- will provide Unicode text field + DateTime, #<- time abstraction field +) +from webhelpers2.text import urlify #<- will generate slugs +from webhelpers2.date import distance_of_time_in_words #<- human friendly dates + + +class BlogRecord(Base): + __tablename__ = 'entries' + id = Column(Integer, primary_key=True) + title = Column(Unicode(255), unique=True, nullable=False) + body = Column(UnicodeText, default='') + created = Column(DateTime, default=datetime.datetime.now) + creator = Column(Unicode(50), default='') + edited = Column(DateTime, default=datetime.datetime.now) + editor = Column(Unicode(50), default='') + tag = Column(Unicode(25)) + status = Column(Unicode(50), default='brouillon') + + @property + def slug(self): + return urlify(self.title) + + @property + def created_in_words(self): + return distance_of_time_in_words(self.created, + datetime.datetime.utcnow()) + + +class Tags(Base): + __tablename__ = 'tags' + id = Column(Integer, primary_key=True) + tag = Column(Unicode(25)) + diff --git a/ctp_blogr/models/meta.py b/ctp_blogr/models/meta.py new file mode 100644 index 0000000..02285b3 --- /dev/null +++ b/ctp_blogr/models/meta.py @@ -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) diff --git a/ctp_blogr/models/user.py b/ctp_blogr/models/user.py new file mode 100644 index 0000000..9f053a2 --- /dev/null +++ b/ctp_blogr/models/user.py @@ -0,0 +1,34 @@ +import datetime #<- will be used to set default dates on models +from ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit +from sqlalchemy import ( + Column, + Integer, + Unicode, #<- will provide Unicode field + UnicodeText, #<- will provide Unicode text field + DateTime, #<- time abstraction field +) + +from passlib.apps import custom_app_context as blogger_pwd_context + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + name = Column(Unicode(255), unique=True, nullable=False) + password = Column(Unicode(255), nullable=False) + last_logged = Column(DateTime, default=datetime.datetime.utcnow) + + def verify_password(self, password): + # is it cleartext? + if password == self.password: + self.set_password(password) + # verify password + result = blogger_pwd_context.verify(password, self.password) + if result: + # pwd OK, set last login date + self.last_logged = datetime.datetime.now() + return result + + def set_password(self, password): + password_hash = blogger_pwd_context.encrypt(password) + self.password = password_hash diff --git a/ctp_blogr/pshell.py b/ctp_blogr/pshell.py new file mode 100644 index 0000000..b0847ee --- /dev/null +++ b/ctp_blogr/pshell.py @@ -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 diff --git a/ctp_blogr/routes.py b/ctp_blogr/routes.py new file mode 100644 index 0000000..42557c1 --- /dev/null +++ b/ctp_blogr/routes.py @@ -0,0 +1,15 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.add_route('apropos', '/apropos') + config.add_route('blog', '/blog/{id:\d+}/{slug}') + config.add_route('blog_bytag', '/blog_bytag/{tag}/{retour}') + config.add_route('blog_edit', '/blog_edit/{id}') + config.add_route('blog_search', '/blog_search') + config.add_route('login', '/login') + config.add_route('logout', '/logout') + config.add_route('tags', '/tags') + config.add_route('tag_edit', '/tag_edit/{id}') + config.add_route('users', '/users') + config.add_route('user_edit', '/user_edit/{name}') + \ No newline at end of file diff --git a/ctp_blogr/scripts/__init__.py b/ctp_blogr/scripts/__init__.py new file mode 100644 index 0000000..5bb534f --- /dev/null +++ b/ctp_blogr/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/ctp_blogr/security.py b/ctp_blogr/security.py new file mode 100644 index 0000000..c3c2ab1 --- /dev/null +++ b/ctp_blogr/security.py @@ -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 + diff --git a/ctp_blogr/services/__init__.py b/ctp_blogr/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ctp_blogr/services/blog_record.py b/ctp_blogr/services/blog_record.py new file mode 100644 index 0000000..337c464 --- /dev/null +++ b/ctp_blogr/services/blog_record.py @@ -0,0 +1,65 @@ +import sqlalchemy as sa +import datetime #<- will be used to set default dates on models + +from sqlalchemy import or_ +from ..models.blog_record import BlogRecord, Tags + + + +class BlogRecordService(object): + + @classmethod + def by_criteria(cls, request, criteria): + search = "%{}%".format(criteria) + query = request.dbsession.query(BlogRecord) + query = query.filter(or_(BlogRecord.title.like(search), BlogRecord.body.like(search))) + if request.authenticated_userid == None: + # if user is anonym, display only published posts + query = query.filter(BlogRecord.status == 'publié') + return query.all() + + @classmethod + def by_id(cls, request, _id): + query = request.dbsession.query(BlogRecord) + return query.get(_id) + + @classmethod + def by_tag(cls, request, tag): + query = request.dbsession.query(BlogRecord) + query = query.filter(BlogRecord.tag == tag) + if request.authenticated_userid == None: + # if user is anonym, display only published posts + query = query.filter(BlogRecord.status == 'publié') + return query.order_by(sa.desc(BlogRecord.created)).all() + + @classmethod + def get_last_created(cls, request): + # gest the 10 last created posts + query = request.dbsession.query(BlogRecord) + if request.authenticated_userid == None: + # if user is anonym, display only published posts + query = query.filter(BlogRecord.status == 'publié') + query = query.order_by(sa.desc(BlogRecord.created)).limit(15).all() + return query + + @classmethod + def delete(cls, request, id): + request.dbsession.query(BlogRecord).filter(BlogRecord.id == id).delete(synchronize_session=False) + return + + @classmethod + def get_tags(cls, request): + query = request.dbsession.query(Tags) + query = query.order_by(Tags.tag).all() + return query + + @classmethod + def get_tags_byId(cls, request, id): + # gest the last 5 items modified + query = request.dbsession.query(Tags).filter(Tags.id == id).first() + return query + + @classmethod + def tag_delete(cls, request, id): + request.dbsession.query(Tags).filter(Tags.id == id).delete(synchronize_session=False) + return diff --git a/ctp_blogr/services/user.py b/ctp_blogr/services/user.py new file mode 100644 index 0000000..2d771f1 --- /dev/null +++ b/ctp_blogr/services/user.py @@ -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 diff --git a/ctp_blogr/static/blog/Conso_2024_en_euro.png b/ctp_blogr/static/blog/Conso_2024_en_euro.png new file mode 100644 index 0000000..efb63ca Binary files /dev/null and b/ctp_blogr/static/blog/Conso_2024_en_euro.png differ diff --git a/ctp_blogr/static/blog/Conso_2024_en_kWh.png b/ctp_blogr/static/blog/Conso_2024_en_kWh.png new file mode 100644 index 0000000..c2a8bad Binary files /dev/null and b/ctp_blogr/static/blog/Conso_2024_en_kWh.png differ diff --git a/ctp_blogr/static/blog/Conso_2025_en_KWh.png b/ctp_blogr/static/blog/Conso_2025_en_KWh.png new file mode 100644 index 0000000..eb15dc3 Binary files /dev/null and b/ctp_blogr/static/blog/Conso_2025_en_KWh.png differ diff --git a/ctp_blogr/static/blog/Conso_2025_en_euro.png b/ctp_blogr/static/blog/Conso_2025_en_euro.png new file mode 100644 index 0000000..c5ef50f Binary files /dev/null and b/ctp_blogr/static/blog/Conso_2025_en_euro.png differ diff --git a/ctp_blogr/static/blog/MacBook_mid_2014.png b/ctp_blogr/static/blog/MacBook_mid_2014.png new file mode 100644 index 0000000..fe4d9b1 Binary files /dev/null and b/ctp_blogr/static/blog/MacBook_mid_2014.png differ diff --git a/ctp_blogr/static/blog/MacBook_mid_2014_sonoma.png b/ctp_blogr/static/blog/MacBook_mid_2014_sonoma.png new file mode 100644 index 0000000..8634f06 Binary files /dev/null and b/ctp_blogr/static/blog/MacBook_mid_2014_sonoma.png differ diff --git a/ctp_blogr/static/blog/my-home-network.png b/ctp_blogr/static/blog/my-home-network.png new file mode 100644 index 0000000..735f780 Binary files /dev/null and b/ctp_blogr/static/blog/my-home-network.png differ diff --git a/ctp_blogr/static/blog/nginx_proxy_reverse.png b/ctp_blogr/static/blog/nginx_proxy_reverse.png new file mode 100644 index 0000000..1b41f30 Binary files /dev/null and b/ctp_blogr/static/blog/nginx_proxy_reverse.png differ diff --git a/ctp_blogr/static/blog/opencode_screen.png b/ctp_blogr/static/blog/opencode_screen.png new file mode 100644 index 0000000..a2c3a72 Binary files /dev/null and b/ctp_blogr/static/blog/opencode_screen.png differ diff --git a/ctp_blogr/static/blog/proxy_certificat.png b/ctp_blogr/static/blog/proxy_certificat.png new file mode 100644 index 0000000..f4e25a9 Binary files /dev/null and b/ctp_blogr/static/blog/proxy_certificat.png differ diff --git a/ctp_blogr/static/blog/proxy_ddns.png b/ctp_blogr/static/blog/proxy_ddns.png new file mode 100644 index 0000000..3fe2e23 Binary files /dev/null and b/ctp_blogr/static/blog/proxy_ddns.png differ diff --git a/ctp_blogr/static/blog/proxy_firewall.jpg b/ctp_blogr/static/blog/proxy_firewall.jpg new file mode 100644 index 0000000..162f64f Binary files /dev/null and b/ctp_blogr/static/blog/proxy_firewall.jpg differ diff --git a/ctp_blogr/static/blog/proxy_inverse.png b/ctp_blogr/static/blog/proxy_inverse.png new file mode 100644 index 0000000..7efe2f8 Binary files /dev/null and b/ctp_blogr/static/blog/proxy_inverse.png differ diff --git a/ctp_blogr/static/blog/proxy_regle.jpg b/ctp_blogr/static/blog/proxy_regle.jpg new file mode 100644 index 0000000..8244ec7 Binary files /dev/null and b/ctp_blogr/static/blog/proxy_regle.jpg differ diff --git a/ctp_blogr/static/blog/proxy_reverse.png b/ctp_blogr/static/blog/proxy_reverse.png new file mode 100644 index 0000000..437d3a7 Binary files /dev/null and b/ctp_blogr/static/blog/proxy_reverse.png differ diff --git a/ctp_blogr/static/blog/vpn_1.jpg b/ctp_blogr/static/blog/vpn_1.jpg new file mode 100644 index 0000000..f5cbe7f Binary files /dev/null and b/ctp_blogr/static/blog/vpn_1.jpg differ diff --git a/ctp_blogr/static/blog/vpn_2.jpg b/ctp_blogr/static/blog/vpn_2.jpg new file mode 100644 index 0000000..e711225 Binary files /dev/null and b/ctp_blogr/static/blog/vpn_2.jpg differ diff --git a/ctp_blogr/static/blog/vpn_3.jpg b/ctp_blogr/static/blog/vpn_3.jpg new file mode 100644 index 0000000..1e05b08 Binary files /dev/null and b/ctp_blogr/static/blog/vpn_3.jpg differ diff --git a/ctp_blogr/static/blog/wonders_cao.png b/ctp_blogr/static/blog/wonders_cao.png new file mode 100644 index 0000000..6abb324 Binary files /dev/null and b/ctp_blogr/static/blog/wonders_cao.png differ diff --git a/ctp_blogr/static/favicon.ico b/ctp_blogr/static/favicon.ico new file mode 100644 index 0000000..1f2210f Binary files /dev/null and b/ctp_blogr/static/favicon.ico differ diff --git a/ctp_blogr/static/mythosaur_skull_480x480.webp b/ctp_blogr/static/mythosaur_skull_480x480.webp new file mode 100644 index 0000000..1274894 Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_480x480.webp differ diff --git a/ctp_blogr/static/mythosaur_skull_60x150.png b/ctp_blogr/static/mythosaur_skull_60x150.png new file mode 100644 index 0000000..a0dd7e1 Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_60x150.png differ diff --git a/ctp_blogr/static/mythosaur_skull_80x80.jpg b/ctp_blogr/static/mythosaur_skull_80x80.jpg new file mode 100644 index 0000000..a420c48 Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_80x80.jpg differ diff --git a/ctp_blogr/static/mythosaur_skull_80x80.png b/ctp_blogr/static/mythosaur_skull_80x80.png new file mode 100644 index 0000000..5da6843 Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_80x80.png differ diff --git a/ctp_blogr/static/mythosaur_skull_red.ico b/ctp_blogr/static/mythosaur_skull_red.ico new file mode 100644 index 0000000..9c34bdd Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_red.ico differ diff --git a/ctp_blogr/static/mythosaur_skull_white.jpg b/ctp_blogr/static/mythosaur_skull_white.jpg new file mode 100644 index 0000000..1f5d7de Binary files /dev/null and b/ctp_blogr/static/mythosaur_skull_white.jpg differ diff --git a/ctp_blogr/static/pyramid-16x16.png b/ctp_blogr/static/pyramid-16x16.png new file mode 100644 index 0000000..9792031 Binary files /dev/null and b/ctp_blogr/static/pyramid-16x16.png differ diff --git a/ctp_blogr/static/pyramid.png b/ctp_blogr/static/pyramid.png new file mode 100644 index 0000000..4ab837b Binary files /dev/null and b/ctp_blogr/static/pyramid.png differ diff --git a/ctp_blogr/static/theme.css b/ctp_blogr/static/theme.css new file mode 100644 index 0000000..671b4c4 --- /dev/null +++ b/ctp_blogr/static/theme.css @@ -0,0 +1,163 @@ +/* Theme inspired from Bootstrap Theme "The Band" */ +/* https://www.w3schools.com/bootstrap/bootstrap_theme_band.asp */ + +body { + font: 100 18px/24px Times New Roman, serif; + color: rgb(90, 89, 89); +} +.container h2, h3{ + color: #df4937 !important; +} +h5 { + margin: 10px 0 30px 0; + letter-spacing: 10px; + font-size: 20px; + color: #111; +} +.person { + border: 10px solid transparent; + margin-bottom: 25px; + width: 80%; + height: 80%; + opacity: 0.7; +} +.person:hover { + border-color: #f1f1f1; +} +.carousel-inner img { + -webkit-filter: grayscale(90%); + filter: grayscale(90%); /* make all photos black and white */ + width: 100%; /* Set width to 100% */ + margin: auto; +} +.carousel-caption h3 { + color: #fff !important; +} +@media (max-width: 600px) { + .carousel-caption { + display: none; /* Hide the carousel text when the screen is less than 600 pixels wide */ + } +} +@media (min-width: 1200px) { + .container{ + max-width: 950px; + } +} +@media print { + /* ne pas affichier l'url after the link */ + a[href]:after { + content: none !important; + } +* { + color: inherit !important; + background-color: inherit !important; + } +} +.bg-1 { + background: #bc2131; + color: #bdbdbd; +} +.bg-1 h3 {color: #fff;} +.bg-1 p {font-style: italic;} +.list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} +.list-group-item:last-child { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.thumbnail { + padding: 0 0 15px 0; + border: none; + border-radius: 0; +} +.monospace-font { + font-family: Monaco, "Courier New", Courier, monospace; +} +.thumbnail p { + margin-top: 15px; + color: #555; +} + +.modal-header, h4, .close { + background-color: #333; + color: #fff !important; + text-align: center; + font-size: 30px; +} +.modal-header, .modal-body { + padding: 40px 50px; +} +.nav-tabs li a { + color: #777; +} +#googleMap { + width: 100%; + height: 400px; + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} +.navbar { + font-family: Montserrat, sans-serif; + margin-bottom: 0; + background-color: #bc2131; + border: 0; + font-size: 14px !important; + letter-spacing: 4px; + opacity: 0.9; +} +.navbar li a, .navbar .navbar-brand { + color: #ffffff !important; +} +.navbar-nav li a:hover { + color: #fff !important; +} +.navbar-nav li.active a { + color: #fff !important; + background-color: #29292c !important; +} +.navbar-default .navbar-toggle { + border-color: transparent; +} + +footer { + background-color: #bc2131; + color: #f5f5f5; + padding: 32px; +} +footer a { + color: #f5f5f5; +} +footer a:hover { + color: #777; + text-decoration: none; +} +.form-control { + border-radius: 0; +} +textarea { + resize: none; +} +.required-field::after { + content: "*"; + color: red; + margin-left:2px; +} + +/* Dropdown */ +.open .dropdown-toggle { + color: #fff ; + background-color: #555 !important; +} + +/* Dropdown links */ +.dropdown-menu li a { + color: #000 !important; +} + +/* On hover, the dropdown links will turn red */ +.dropdown-menu li a:hover { + background-color: red !important; +} + diff --git a/ctp_blogr/templates/404.jinja2 b/ctp_blogr/templates/404.jinja2 new file mode 100644 index 0000000..4a08495 --- /dev/null +++ b/ctp_blogr/templates/404.jinja2 @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

CTP Blog

+

404 Page Not Found

+
+{% endblock content %} diff --git a/ctp_blogr/templates/apropos.jinja2 b/ctp_blogr/templates/apropos.jinja2 new file mode 100644 index 0000000..bc08d18 --- /dev/null +++ b/ctp_blogr/templates/apropos.jinja2 @@ -0,0 +1,33 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+
+
+
+
+
+ L'argent qu'on possède est l'instrument de la liberté; celui qu'on pourchasse est celui de la servitude. +
+
+ +
+
+
+
+
+
+ L'intelligence ce n'est pas ce que l'on sait mais ce que l'on fait quand on ne sait pas. +
+
+
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/ctp_blogr/templates/blog.jinja2 b/ctp_blogr/templates/blog.jinja2 new file mode 100644 index 0000000..17ef589 --- /dev/null +++ b/ctp_blogr/templates/blog.jinja2 @@ -0,0 +1,33 @@ +{% extends "layout.jinja2" %} + +{% block content %} + {% if request.authenticated_userid %} +

+ [ Retour ] + [ Modifier ] + +

+ {% endif %} + +
+

{{ body_html | safe }}

+
+

+ Publié le : {{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}
+ {% if request.authenticated_userid %} + Modifié le : {{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}
+ Tag : {{ entry.tag }}
+ Statut : {{ entry.status }} + {% endif %} +

+ + + +{% endblock %} + diff --git a/ctp_blogr/templates/blog_bytag.jinja2 b/ctp_blogr/templates/blog_bytag.jinja2 new file mode 100644 index 0000000..ce1e06a --- /dev/null +++ b/ctp_blogr/templates/blog_bytag.jinja2 @@ -0,0 +1,26 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +

[ retour ]

+ + + {% for entry in items %} + + + + {% if entry.status != 'publié' %} + + {% else %} + + {% endif%} + + {% else %} +

Aucun post trouvé !

+ {% endfor %} +
{{ entry.created.strftime("%d %b %y") }} + {{ entry.title }} + {{ entry.status }} 
+

[ {{nb_items}} billets ]

+ +{% endblock %} diff --git a/ctp_blogr/templates/blog_edit.jinja2 b/ctp_blogr/templates/blog_edit.jinja2 new file mode 100644 index 0000000..7069f9f --- /dev/null +++ b/ctp_blogr/templates/blog_edit.jinja2 @@ -0,0 +1,82 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+ + {% for error in form.title.errors %} +
{{ error }}
+ {% endfor %} +
+ + {{ form.title(class_='form-control') }} +
+ + {% for error in form.body.errors %} +
{{ error }}
+ {% endfor %} +
+ + {{ form.body(class_='form-control monospace-font', cols="35", rows="20") }} +
+ +
+ + {{ form.tag(class_='form-control') }} +
+ +
+ + {{ form.status(class_='form-control') }} +
+ +

+ {% if blog_id != '0' %} + Créé le : {{ entry.created.strftime("%d-%m-%Y - %H:%M") }} + +
+ Modifié le : {{ entry.edited.strftime("%d-%m-%Y - %H:%M") }} + {% endif %} +

+ +
+
+ Retour + + {% if blog_id != '0' %} + + {% endif %} +
+ +

Apprendre la syntaxe de Markdown

+ +
+ + + + + {% endblock %} diff --git a/ctp_blogr/templates/blog_search.jinja2 b/ctp_blogr/templates/blog_search.jinja2 new file mode 100644 index 0000000..6362bdb --- /dev/null +++ b/ctp_blogr/templates/blog_search.jinja2 @@ -0,0 +1,57 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+ +
+
+
+ {{ form.criteria(class_='form-control') }} + + + +
+ {% for error in form.criteria.errors %} +
{{ error }}
+ {% endfor %} +
+
+
+ +
+ {% if items %} + + + + + + + + + + {% for entry in items %} + + + + + + {% endfor %} +
TitreTagsDate
+ + {{ entry.title }} + + {{ entry.tag }}{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}
+ {% endif %} +
+
+

Index

+

+ |  + {% for tag in tags %} + {{ tag.tag }} | + {% endfor %} +

+ +{% endblock %} diff --git a/ctp_blogr/templates/home.jinja2 b/ctp_blogr/templates/home.jinja2 new file mode 100644 index 0000000..b240f5c --- /dev/null +++ b/ctp_blogr/templates/home.jinja2 @@ -0,0 +1,43 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+

{{ cartoon_title }}

+

+
+ © Chappatte +

+
+ +
+ {% if request.authenticated_userid %} +

+ [ Nouveau ] + [ Sélectionner un autre le dessin ] +

+ {% endif%} + + + {% for entry in last_ten %} + + + + + {% if entry.status != 'publié' %} + + {% else %} + + {% endif%} + + {% else %} +

Aucun post trouvé

+ {% endfor %} +
{{ entry.created.strftime("%d %b %y") }} + {{ entry.title }} + + [ {{ entry.tag }} ] + {{ entry.status }} 
+ +{% endblock %} diff --git a/ctp_blogr/templates/layout.jinja2 b/ctp_blogr/templates/layout.jinja2 new file mode 100644 index 0000000..92bf715 --- /dev/null +++ b/ctp_blogr/templates/layout.jinja2 @@ -0,0 +1,109 @@ + + + + + + + + + + {{page_title}} + + + + + + + + + + + + + + + + +
+
+
+ + {% if page_title %} +

{{ page_title }}

+ {% endif %} +
+
+ {% for queue in ['', 'info', 'success', 'warning', 'danger'] %} + {% for message in request.session.pop_flash(queue) %} +
+ + {{ message }} +
+ {% endfor %} + {% endfor %} +
+ + + {% block content %} +

No content

+ {% endblock content %} +
+
+
+ + + + + + + + + + diff --git a/ctp_blogr/templates/login.jinja2 b/ctp_blogr/templates/login.jinja2 new file mode 100644 index 0000000..483bdbe --- /dev/null +++ b/ctp_blogr/templates/login.jinja2 @@ -0,0 +1,30 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+
+ +
+

Se connecter

+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +{% endblock %} diff --git a/ctp_blogr/templates/tag_edit.jinja2 b/ctp_blogr/templates/tag_edit.jinja2 new file mode 100644 index 0000000..7369c72 --- /dev/null +++ b/ctp_blogr/templates/tag_edit.jinja2 @@ -0,0 +1,55 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +
+ + {% for error in form.tag.errors %} +
{{ error }}
+ {% endfor %} + +
+ + {{form.tag(class_='form-control')}} +
+ +
+ + Retour + + {% if form.id.data %} + + {% endif %} +
+ + +
+ + + + +{% endblock %} diff --git a/ctp_blogr/templates/tags.jinja2 b/ctp_blogr/templates/tags.jinja2 new file mode 100644 index 0000000..1e1460b --- /dev/null +++ b/ctp_blogr/templates/tags.jinja2 @@ -0,0 +1,26 @@ +{% extends "layout.jinja2" %} + +{% block content %} + +

+ Nouveau +

+ + + + + + + + + {% for entry in tags %} + + + + + {% endfor %} +
No IdTag
{{ entry.id }} + {{ entry.tag }} +
+ +{% endblock %} diff --git a/ctp_blogr/templates/user_edit.jinja2 b/ctp_blogr/templates/user_edit.jinja2 new file mode 100644 index 0000000..929cc95 --- /dev/null +++ b/ctp_blogr/templates/user_edit.jinja2 @@ -0,0 +1,55 @@ +{% extends "layout.jinja2" %} + +{% block content %} + + {% if message %} +
+ {{ message }} +
+ {% endif %} + +
+ + {% for error in form.name.errors %} +
{{ error }}
+ {% endfor %} + +
+ + {% if form.id.data %} + + {% else %} + {{form.name(class_='form-control')}} + {% endif %} +
+ +
+ + {{form.password(class_='form-control')}} +
+ + {% for error in form.confirm.errors %} +
{{error}}
+ {% endfor %} + +
+ + {{form.confirm(class_='form-control')}} +
+ +
+
+ Retour + + {% if form.id.data and request.authenticated_userid == 'admin' %} + + {% endif %} + +
+ + +
+ +{% endblock %} diff --git a/ctp_blogr/templates/users.jinja2 b/ctp_blogr/templates/users.jinja2 new file mode 100644 index 0000000..46d697c --- /dev/null +++ b/ctp_blogr/templates/users.jinja2 @@ -0,0 +1,32 @@ +{% extends "layout.jinja2" %} + +{% block content %} +

+ + Retour + + Nouvel utilisateur +

+ + + + + + + + + + {% for entry in users %} + + + + + + {% endfor %} +
No IdNomDernière connexion
{{ entry.id }} + + {{ entry.name }} + + {{ entry.last_logged.strftime("%d-%m-%Y - %H:%M") }}
+ +{% endblock %} diff --git a/ctp_blogr/tests.py b/ctp_blogr/tests.py new file mode 100644 index 0000000..fe46681 --- /dev/null +++ b/ctp_blogr/tests.py @@ -0,0 +1,56 @@ +import unittest + +from pyramid import testing + +import transaction + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def test_passing_view(self): + from .views.default import apropos + response = apropos(dummy_request(self.session)) + self.assertEqual(response['page_title'], 'A propos') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import apropos + response = apropos(dummy_request(self.session)) + self.assertEqual(response['page_title'], 'A propos') diff --git a/ctp_blogr/views/__init__.py b/ctp_blogr/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ctp_blogr/views/blog.py b/ctp_blogr/views/blog.py new file mode 100644 index 0000000..fb587d8 --- /dev/null +++ b/ctp_blogr/views/blog.py @@ -0,0 +1,188 @@ +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPNotFound, HTTPFound +from ..models.blog_record import BlogRecord, Tags +from ..services.blog_record import BlogRecordService +from ..forms import BlogCreateForm, BlogUpdateForm, BlogSearchForm, TagForm +import markdown +import datetime #<- will be used to set default dates on models + +@view_config(route_name='blog', renderer='../templates/blog.jinja2') +def blog(request): + # get post id from request + blog_id = request.matchdict['id'] + entry = BlogRecordService.by_id(request, blog_id) + + if not entry: + request.session.flash(u"Page non trouvée : %s" % blog_id, 'warning') + return HTTPFound(location=request.route_url('home')) + + # insèrer le path de static/img + body = entry.body.replace('static/', "%s/static/" % request.application_url) + # convertir de markdown en HTML + body_html = markdown.markdown(body, extensions=['footnotes']) + return_url = "/" + + return { + 'page_title': entry.title, + 'entry': entry, + 'body_html': body_html, + 'return_url': return_url, + } + + +@view_config(route_name='blog_edit', renderer='../templates/blog_edit.jinja2', permission='view') +def blog_edit(request): + # get post id from request + blog_id = request.matchdict['id'] + url = request.route_url('blog_edit',id=blog_id) + + # get the list of tags + tags = BlogRecordService.get_tags(request) + + if blog_id == '0': + # create a new post + entry = BlogRecord() + # set default values + form = BlogCreateForm(request.POST, entry) + form.tag.choices = [(row.tag, row.tag) for row in tags] + page_title = 'Nouvelle page' + else: + # modify post + entry = BlogRecordService.by_id(request, blog_id) + if not entry: + request.session.flash(u"Page non trouvée : %s" % blog_id, 'warning') + return HTTPFound(location=request.route_url('home')) + form = BlogUpdateForm(request.POST, entry) + form.tag.choices = [(row.tag, row.tag) for row in tags] + page_title = 'Modifier : ' + entry.title + + if 'form.submitted' in request.params and form.validate(): + if blog_id == '0': + form.populate_obj(entry) + # interdire le car '/' dans le titre à cause du slug + entry.title = entry.title.replace('/','.') + entry.creator = request.authenticated_userid + entry.editor = entry.creator + request.dbsession.add(entry) + + return HTTPFound(location=request.route_url('home')) + else: + del form.id # SECURITY: prevent overwriting of primary key + form.populate_obj(entry) + # interdire le car '/' dans le titre à cause du slug + entry.title = entry.title.replace('/','.') + entry.edited = datetime.datetime.now() + entry.editor = request.authenticated_userid + return HTTPFound(location=request.route_url('blog', id=entry.id, slug=entry.slug)) + + if 'form.update_created' in request.params and form.validate(): + del form.id # SECURITY: prevent overwriting of primary key + form.populate_obj(entry) + entry.created = datetime.datetime.now() + return HTTPFound(location=request.route_url('blog', id=entry.id, slug=entry.slug)) + + if 'form.deleted' in request.params: + BlogRecordService.delete(request, blog_id) + request.session.flash("La page a été supprimée avec succès.", 'success') + return HTTPFound(location=request.route_url('home')) + + return { + 'page_title': page_title, + 'url': url, + 'form': form, + 'blog_id': blog_id, + 'entry': entry, } + +@view_config(route_name='blog_bytag', renderer='../templates/blog_bytag.jinja2') +def blog_bytag(request): + # get tag parameters from request + tag = request.matchdict['tag'] + retour = request.matchdict['retour'] + url_retour = request.route_url(retour) + + # get the posts by tag + items = BlogRecordService.by_tag(request, tag) + nb_items = len(items) + + return { + 'page_title': "Billets avec tag : " + tag, + 'items': items, + 'nb_items': nb_items, + 'url_retour': url_retour, + } + + +@view_config(route_name='blog_search', renderer='../templates/blog_search.jinja2') +def blog_search(request): + + criteria = '' + # get the list of tags + tags = BlogRecordService.get_tags(request) + + form = BlogSearchForm(request.POST) + items = [] + if 'form.submitted' in request.params and form.validate(): + criteria = request.params['criteria'] + # si afficher tous les fiches ? + items = BlogRecordService.by_criteria(request, criteria) + + return { + 'page_title': "Rechercher", + 'form': form, + 'items': items, + 'criteria': criteria, + 'tags': tags, + } + +@view_config(route_name='tags', renderer='../templates/tags.jinja2', permission='view') +def tags(request): + + # get the list of tags + tags = BlogRecordService.get_tags(request) + + return { + 'page_title': 'Tags', + 'tags': tags, + } + +@view_config(route_name='tag_edit', renderer='../templates/tag_edit.jinja2', permission='view') +def tag_edit(request): + # get tag parameters from request + tag_id = request.matchdict['id'] + url = request.route_url('tag_edit', id=tag_id) + + if tag_id == '0': + # create a new tag + entry = Tags() + form = TagForm(request.POST, entry) + page_title = "Nouveau Tag" + + else: + # modify post + entry = BlogRecordService.get_tags_byId(request, tag_id) + if not entry: + request.session.flash(u"Tag non trouvé : %s" % tag_id, 'warning') + return HTTPFound(location=request.route_url('tags')) + form = TagForm(request.POST, entry) + page_title = entry.tag + + if 'form.submitted' in request.params and form.validate(): + if tag_id == '0': + form.populate_obj(entry) + request.dbsession.add(entry) + return HTTPFound(location=request.route_url('tags')) + else: + del form.id # SECURITY: prevent overwriting of primary key + form.populate_obj(entry) + return HTTPFound(location=request.route_url('tags')) + + if 'form.deleted' in request.params: + BlogRecordService.tag_delete(request, entry.id) + request.session.flash("La fiche a été supprimée avec succès.", 'success') + return HTTPFound(location=request.route_url('tags')) + + return { + 'page_title': page_title, + 'url': url, + 'form': form, + } diff --git a/ctp_blogr/views/default.py b/ctp_blogr/views/default.py new file mode 100644 index 0000000..5d31b2a --- /dev/null +++ b/ctp_blogr/views/default.py @@ -0,0 +1,153 @@ +from pyramid.view import ( + view_config, + forbidden_view_config, +) +from pyramid.httpexceptions import HTTPFound +from pyramid.security import remember, forget +from ..services.user import UserService +from ..services.blog_record import BlogRecordService +from ..forms import UserCreateForm +from ..models.user import User + + +@view_config(route_name='home', renderer='../templates/home.jinja2') +def home(request): + # get the last created posts + last_ten = BlogRecordService.get_last_created(request) + # get Chappatte cartoon + entry = BlogRecordService.by_id(request, 95) + + return { + 'page_title': "This is the Way", + 'last_ten': last_ten, + 'cartoon_title': entry.title, + 'cartoon_url': entry.body, + } + + +@view_config(route_name='apropos', + renderer='../templates/apropos.jinja2') +def apropos(request): + + return { + 'page_title': "A propos", + } + + +@view_config(route_name='login', renderer='../templates/login.jinja2') +@forbidden_view_config(renderer='../templates/login.jinja2') +def login(request): + login_url = request.route_url('login') + + referrer = request.url + if referrer == login_url: + referrer = '/' # never use the login form itself as came_from + + came_from = request.params.get('came_from', referrer) + username = '' + userpwd = '' + if 'form.submitted' in request.params: + username = request.POST.get('username') + userpwd = request.POST.get('password') + user = UserService.by_name(request, username) + if user and user.verify_password(userpwd): + headers = remember(request, username) + request.session.flash("Bienvenue %s !" % username, 'success') + return HTTPFound(location=came_from, headers=headers) + else: + headers = forget(request) + request.session.flash("Login et mot de passe invalides. La connexion a échoué.", "danger") + + return { + 'page_title': "", + 'came_from': came_from, + 'login_url': login_url, + } + + +@view_config(route_name='logout', renderer='string') +def logout(request): + username = request.authenticated_userid + if username == None: + username = '' + + headers = forget(request) + request.session.flash('Au revoir ' + username + ' !', 'success') + return HTTPFound(location=request.route_url('home'), headers=headers) + + +@view_config(route_name='users', renderer='../templates/users.jinja2', permission='manage') +def users(request): + # get all users + users = UserService.all(request) + return { + 'page_title': "Liste des utilisateurs", + 'users': users + } + +@view_config(route_name='user_edit', renderer='ctp_blogr:templates/user_edit.jinja2', permission='view') +def user_edit(request): + message = '' + name = request.matchdict['name'] + url = request.route_url('user_edit', name=name) + if request.authenticated_userid == 'admin': + url_retour = request.route_url('users') + else: + url_retour = request.route_url('home') + + if name == '0': + # nouvel utilisateur + user = User() + form = UserCreateForm(request.POST, user) + page_title = "Nouvel utilisateur" + else: + # lire la fiche du user + user = UserService.by_name(request, name) + if not user: + request.session.flash("Utilisateur non trouvé : %s" % name, 'danger') + return HTTPFound(location=url_retour) + + form = UserCreateForm(request.POST, user) + page_title = "Modification utilisateur" + + + if 'form.submitted' in request.params and form.validate(): + # controle que le password a moins 6 car + if len(form.password.data) < 6 : + message = "Le mot de passe doit avoir au moins 6 caractères" + else: + if name == '0': + # création user + # controler que le nouvel user n'existe pas dans la BD + new_user = UserService.by_name(request, form.name.data) + if new_user: + message = "Utilisateur déjà créé : %s" % form.name.data + else: + form.populate_obj(user) + user.set_password(form.password.data.encode('utf8')) + # créer le nouveau + request.dbsession.add(user) + request.session.flash("La fiche a été créée avec succès.", 'success') + return HTTPFound(location=url_retour) + + else: + # modification user + del form.name # SECURITY: prevent overwriting of primary key + form.populate_obj(user) + user.set_password(form.password.data.encode('utf8')) + request.session.flash("La fiche a été modifiée avec succès.", 'success') + return HTTPFound(location=url_retour) + + if 'form.deleted' in request.params: + delete(request, user.id) + request.session.flash("La fiche a été supprimée avec succès.", 'success') + return HTTPFound(location=url_retour) + + return { + 'page_title': page_title, + 'message': message, + 'form': form, + 'url': url, + 'url_retour': url_retour, + 'name': name, + } diff --git a/ctp_blogr/views/notfound.py b/ctp_blogr/views/notfound.py new file mode 100644 index 0000000..69d6e28 --- /dev/null +++ b/ctp_blogr/views/notfound.py @@ -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 {} diff --git a/development.ini b/development.ini new file mode 100644 index 0000000..b51d589 --- /dev/null +++ b/development.ini @@ -0,0 +1,74 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:ctp_blogr + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite + +retry.attempts = 3 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +[pshell] +setup = ctp_blogr.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:9280 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, ctp_blogr, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_ctp_blogr] +level = DEBUG +handlers = +qualname = ctp_blogr + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/production.ini b/production.ini new file mode 100644 index 0000000..031b8cb --- /dev/null +++ b/production.ini @@ -0,0 +1,69 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html +### + +[app:main] +use = egg:ctp_blogr + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite + +retry.attempts = 3 + +[pshell] +setup = ctp_blogr.pshell.setup + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:9280 +url_scheme = https + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html +### + +[loggers] +keys = root, ctp_blogr, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_ctp_blogr] +level = WARN +handlers = +qualname = ctp_blogr + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..27fabfd --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +testpaths = ctp_blogr +python_files = test*.py diff --git a/rtd.txt b/rtd.txt new file mode 100644 index 0000000..93b76e4 --- /dev/null +++ b/rtd.txt @@ -0,0 +1 @@ +pylons-sphinx-themes diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6285330 --- /dev/null +++ b/setup.py @@ -0,0 +1,64 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.md')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'plaster_pastedeploy', + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + 'pyramid_retry', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'wtforms', # form library 2.2.1 + 'webhelpers2', # various web building related helpers 2.0 + 'passlib', + 'markdown', +] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest >= 3.7.4', + 'pytest-cov', +] + +setup( + name='ctp_blogr', + version='1.0', + description='ctp_blogr', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + 'Programming Language :: Python', + 'Framework :: Pyramid', + 'Topic :: Internet :: WWW/HTTP', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points={ + 'paste.app_factory': [ + 'main = ctp_blogr:main', + ], + 'console_scripts': [ + 'initialize_ctp_blogr_db=ctp_blogr.scripts.initialize_db:main', + ], + }, +)