Compare commits

49 Commits

Author SHA1 Message Date
Phuoc Cao
181d8d0d21 correction du nom MacBook_mid_2014_sonoma.png 2025-12-13 18:50:24 +01:00
Phuoc Cao
ebfe0ba995 chage sequoia to sonoma 2025-12-13 17:42:24 +01:00
91ee548b52 update mac about image 2025-11-30 15:51:19 +01:00
3c620d6142 new mac about image 2025-11-30 15:26:47 +01:00
dcb8ac5b30 image BigSur modified 2025-11-29 16:43:59 +01:00
ac4e073e49 add image BigSur 2025-11-29 12:19:26 +01:00
e12e7e7543 commentaires en francais dans le script 2025-09-09 15:30:30 +02:00
8eb55385ad my home network image 2025-08-24 15:01:17 +02:00
46a7f30bae updated visited wonders image 2025-07-02 08:27:14 +02:00
c6c14938e4 tuning home page image size 2025-05-31 16:18:45 +02:00
63e776e370 fixed bug 2025-05-26 18:06:48 +02:00
93833d527a update SQLAlchemy version 2024-12-04 16:51:30 +01:00
d3c66e4240 100 wonders image 2024-11-13 09:21:30 +01:00
f90649d3cd bug error in logout when timeout reached 2024-06-20 19:24:28 +02:00
11318a32e3 update 100 wonders list 2024-06-17 10:01:12 +02:00
6af19b99c1 save database 2024-01-08 13:29:15 +07:00
985354d532 fixed bug in delete actif and create allocation 2024-01-08 06:51:21 +01:00
2963f8e5c5 commit database 2023-10-24 16:31:04 +02:00
dd231c0d64 added static/blog folder for images 2023-10-24 16:05:41 +02:00
3efce74696 added fixed font to blog_edit textaera 2023-10-22 09:22:33 +02:00
51169bb96b nginx reverse proxy 2023-08-12 11:14:45 +02:00
9fb5ea0e0d change line spacing" 2023-08-05 14:13:08 +02:00
85f8423a39 merge user_add and user_pwd into user_edit 2023-06-28 16:55:36 +02:00
f4d603750e rename app name to ctp_blogr 2023-06-22 16:03:20 +02:00
8b2fcb1a76 changed family-font to Times sans rerif 2023-05-31 16:00:28 +02:00
5ce9066824 added scripts for testing smtp and imap 2023-05-28 20:16:28 +02:00
92142854df ajout des images 2023-05-13 13:19:19 +02:00
6c146e66c9 reduced logo size 2023-05-06 10:25:31 +02:00
2d5e8fc20e add mythosaur skull 2023-05-05 17:56:11 +02:00
7e73994acf add link to quote website 2023-05-03 17:00:28 +02:00
b714c6138a added update created date in blog_edit 2023-03-25 11:12:48 +01:00
4103d08e26 remove ticker.info no longer exist 2023-03-24 11:57:00 +01:00
fdcf474bde added copyriht link under chapatte cartoon 2023-02-26 09:50:42 +01:00
b7a8c64333 added Chappatte cartoon on homepage 2023-02-24 16:12:12 +01:00
7a2ba16e2f ignore cao_blogr.sqlite 2023-02-17 11:49:53 +01:00
05c3d83ae0 remove set_trace 2023-02-17 11:27:23 +01:00
e03b2ea353 add modified date to portfolio 2023-02-13 07:35:08 +01:00
1e9cb12565 changed home page title 2023-02-10 11:15:15 +01:00
d712e5e4db add https url_schema in production 2023-02-10 09:29:26 +01:00
1e6ec919e0 round all calculation to 2 digits 2023-02-10 08:59:19 +01:00
f1287c3fad added showing blogs by tag 2023-02-09 21:53:51 +01:00
fcd83c48d7 finalized update portfolio 2023-02-09 14:07:24 +01:00
d95ae8eeeb fixed histo_edit 2023-02-08 17:41:07 +01:00
7059553a03 added came_from to login.jinja2 2023-02-08 11:10:41 +01:00
e60212239e added actif_edit template 2023-01-30 15:53:59 +01:00
f8023701a4 joining table allocation to actifs 2023-01-29 18:11:40 +01:00
4a55f94551 added portfolio view 2023-01-26 16:22:58 +01:00
060b796636 added historique 2023-01-23 17:16:45 +01:00
25c6ad3cc0 Merge branch 'release-1.0' 2023-01-22 12:01:55 +01:00
89 changed files with 2009 additions and 358 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
*.pyc
*$py.class
*~
*.sqlite
.coverage
coverage.xml
build/

Binary file not shown.

View File

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

View File

@@ -1,34 +0,0 @@
from wtforms import Form, StringField, TextAreaField, SelectField, validators
from wtforms import IntegerField, PasswordField
from wtforms.validators import InputRequired, Length
from wtforms.widgets import HiddenInput
strip_filter = lambda x: x.strip() if x else None
class BlogCreateForm(Form):
title = StringField('Titre', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
body = TextAreaField('Corps du texte', validators=[InputRequired(), Length(min=1)],
filters=[strip_filter])
tag = SelectField('Tag')
status = SelectField('Statut', choices=[('brouillon','Brouillon'),('privé','Privé'),('publié','Publié')])
class BlogUpdateForm(BlogCreateForm):
id = IntegerField(widget=HiddenInput())
class BlogSearchForm(Form):
criteria = StringField('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):
username = StringField('Nom', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
password = PasswordField('Mot de passe', validators=[InputRequired(), Length(min=6)])

View File

@@ -1,30 +0,0 @@
{% extends "layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<p><a href="{{ request.route_url('blog_edit', id='0') }}" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-plus"></span> Nouveau</a>
</p>
{% endif%}
<table id="posts_list" class="table table-condensed">
{% for entry in last_ten %}
<tr>
<td>{{ entry.created.strftime("%d.%m.%Y") }}</td>
<td>
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
</td>
<td>{{ entry.tag }}</td>
{% if entry.status != 'publié' %}
<td><span class="label label-danger">{{ entry.status }}</span></td>
{% else %}
<td>&nbsp;</td>
{% endif%}
</tr>
{% else %}
<p class="text-danger">Aucun post trouvé</p>
{% endfor %}
</table>
{% endblock %}

View File

@@ -1,34 +0,0 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{request.route_url('user_add', name=name)}}" method="post" class="form">
{% for error in form.username.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="username">{{form.username.label}}</label>
{{form.username(class_='form-control')}}
</div>
{% for error in form.password.errors %}
<div class="error">{{error}}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="password">{{form.password.label}}</label>
{{form.password(class_='form-control')}}
</div>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('users') }}"><span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
</div>
</form>
{% endblock %}

View File

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

View File

@@ -1,127 +0,0 @@
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='cao_blogr:templates/home.jinja2')
def home(request):
# get the last created posts
last_ten = BlogRecordService.get_last_created(request)
return {
'page_title': "Bienvenue sur mon blog",
'last_ten': last_ten,
}
@view_config(route_name='apropos',
renderer='cao_blogr:templates/apropos.jinja2')
def apropos(request):
return {
'page_title': "A propos",
}
@view_config(route_name='login', renderer='cao_blogr:templates/login.jinja2')
@forbidden_view_config(renderer='cao_blogr:templates/login.jinja2')
def login(request):
username = ''
login_url = request.route_url('login')
referrer = request.url
if referrer == login_url:
referrer = '/' # never use the login form itself as came_from
came_from = request.params.get('came_from', referrer)
username = request.POST.get('username')
userpwd = request.POST.get('password')
if username:
user = UserService.by_name(request, username)
if user and user.verify_password(userpwd):
headers = remember(request, username)
request.session.flash("Bienvenue %s !" % username, 'success')
return HTTPFound(location=came_from, headers=headers)
else:
headers = forget(request)
request.session.flash("Login et mot de passe invalides. La connexion a échoué.", "danger")
return {
'page_title': "",
'came_from': came_from,
'login_url': login_url,
}
@view_config(route_name='logout', renderer='string')
def logout(request):
username = request.authenticated_userid
headers = forget(request)
request.session.flash('Au revoir ' + username + ' !', 'success')
return HTTPFound(location=request.route_url('home'), headers=headers)
@view_config(route_name='users', renderer='cao_blogr:templates/users.jinja2', permission='manage')
def users(request):
# get all users
users = UserService.all(request)
return {
'page_title': "Liste des utilisateurs",
'users': users
}
@view_config(route_name='user_add', renderer='cao_blogr:templates/user_add.jinja2', permission='manage')
def user_add(request):
name = request.matchdict['name']
# nouveau
form = UserCreateForm(request.POST)
if 'form.submitted' in request.params and form.validate():
# créer nouveau
new_user = User(name=form.username.data)
new_user.set_password(form.password.data.encode('utf8'))
request.dbsession.add(new_user)
return HTTPFound(location=request.route_url('users'))
return {
'page_title': 'Nouvel utilsateur',
'form': form,
'name': name,
}
@view_config(route_name='user_pwd', renderer='cao_blogr:templates/user_pwd.jinja2', permission='manage')
def user_pwd(request):
# reset password or delete user
name = request.matchdict['name']
# lire la fiche du membre
entry = UserService.by_name(request, name)
if not entry:
request.session.flash(u"Utilisateur non trouvé : %s" % name, 'warning')
return HTTPFound(location=request.route_url('users'))
if 'form.submitted' in request.params:
mdp = request.params["new_password"]
entry.set_password(mdp.encode('utf8'))
return HTTPFound(location=request.route_url('users'))
if 'form.deleted' in request.params:
UserService.delete(request, entry.id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('users'))
return {
'page_title': "Utilisateur : %s" %(entry.name),
'entry': entry,
}

BIN
ctp_blogr.sqlite Normal file

Binary file not shown.

View File

@@ -10,13 +10,13 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
# session factory
my_session_factory = SignedCookieSessionFactory('mGcAJn2HmNH6Hc')
my_session_factory = SignedCookieSessionFactory('hZug2zPt7hT2MZ')
authentication_policy = AuthTktAuthenticationPolicy('wMWvAWMZnp6Lch',
authentication_policy = AuthTktAuthenticationPolicy('J2wv322aL5DTn2',
callback=groupfinder, hashalg='sha512', timeout=36000)
authorization_policy = ACLAuthorizationPolicy()
with Configurator(settings=settings,
root_factory='cao_blogr.security.RootFactory',
root_factory='ctp_blogr.security.RootFactory',
authentication_policy=authentication_policy,
authorization_policy=authorization_policy) as config:
config.include('pyramid_jinja2')

View File

@@ -3,7 +3,7 @@ from alembic import context
from pyramid.paster import get_appsettings, setup_logging
from sqlalchemy import engine_from_config
from cao_blogr.models.meta import Base
from ctp_blogr.models.meta import Base
config = context.config

View File

@@ -0,0 +1,42 @@
"""added foreign key to allocation
Revision ID: b8f8216d72c7
Revises:
Create Date: 2023-01-29 12:20:48.097107
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b8f8216d72c7'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('allocation',
sa.Column('no_cat', sa.Integer(), nullable=False),
sa.Column('classe', sa.Unicode(length=45), nullable=True),
sa.Column('pc_cible', sa.Integer(), nullable=True),
sa.Column('pc_atteint', sa.Float(), nullable=True),
sa.Column('pc_ecart', sa.Float(), nullable=True),
sa.Column('valeur', sa.Float(), nullable=True),
sa.Column('type', sa.Unicode(length=45), nullable=True),
sa.Column('ordre', sa.Integer(), nullable=True),
sa.Column('bg_color', sa.Unicode(length=45), nullable=True),
sa.ForeignKeyConstraint(['classe'], ['actifs.classe'], name=op.f('fk_allocation_classe_actifs')),
sa.PrimaryKeyConstraint('no_cat', name=op.f('pk_allocation'))
)
op.create_index('classe_index', 'allocation', ['classe'], unique=False)
op.create_index('ordre_index', 'allocation', ['ordre'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ordre_index', table_name='allocation')
op.drop_index('classe_index', table_name='allocation')
op.drop_table('allocation')
# ### end Alembic commands ###

66
ctp_blogr/forms.py Normal file
View File

@@ -0,0 +1,66 @@
from wtforms import Form, StringField, TextAreaField, SelectField, DecimalField
from wtforms import IntegerField, PasswordField
from wtforms.validators import InputRequired, Length, EqualTo
from wtforms.widgets import HiddenInput
strip_filter = lambda x: x.strip() if x else None
class BlogCreateForm(Form):
title = StringField('Titre', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
body = TextAreaField('Corps du texte', validators=[InputRequired(), Length(min=1)],
filters=[strip_filter])
tag = SelectField('Tag')
status = SelectField('Statut', choices=[('brouillon','Brouillon'),('privé','Privé'),('publié','Publié')])
class BlogUpdateForm(BlogCreateForm):
id = IntegerField(widget=HiddenInput())
class BlogSearchForm(Form):
criteria = StringField('Critère', validators=[InputRequired(), Length(min=3, max=45)],
filters=[strip_filter])
class TagForm(Form):
id = IntegerField(widget=HiddenInput())
tag = StringField('Tag', validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter])
class UserCreateForm(Form):
id = IntegerField(widget=HiddenInput())
name = StringField('Nom', validators=[InputRequired(), Length(min=1, max=255)],
filters=[strip_filter])
password = PasswordField('Mot de passe')
confirm = PasswordField('Confirmer', validators=[EqualTo('password', message='Les 2 Passwords doivent être identiques')])
class HistoForm(Form):
no_id = IntegerField(widget=HiddenInput())
mvt_cash = DecimalField('Montant à ajouter au portefeuille', places=2, validators=[InputRequired()])
class AllocationForm(Form):
no_cat = IntegerField(widget=HiddenInput())
classe = StringField("Classe d'actif", validators=[InputRequired(), Length(min=1, max=25)], filters=[strip_filter])
pc_cible = IntegerField(validators=[InputRequired()])
type = SelectField('Type', choices=[('ACTION','ACTION'),('AUTRE','AUTRE')])
ordre = IntegerField(validators=[InputRequired()])
bg_color = SelectField('Couleur de fond', choices=[('info','BLEU'),('danger','ROUGE'),('warning','ORANGE'),('success','VERT')])
class ActifForm(Form):
no_id = IntegerField(widget=HiddenInput())
classe = SelectField('Classe')
symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter])
libelle = StringField('Nom', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter])
nombre = IntegerField(validators=[InputRequired()])
pru = DecimalField('PRU', places=3, validators=[InputRequired()])
ter = DecimalField('TER', places=2, validators=[InputRequired()])
pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()])
website = StringField('Web site', validators=[Length(min=1, max=100)], filters=[strip_filter])
class Actif2Form(Form):
no_id = IntegerField(widget=HiddenInput())
classe = SelectField('Classe')
symbole = StringField('Symbole', validators=[InputRequired(), Length(min=1, max=15)], filters=[strip_filter])
libelle = StringField('Libellé', validators=[InputRequired(), Length(min=1, max=45)], filters=[strip_filter])
pru = DecimalField('Total investi', places=3, validators=[InputRequired()])
cours = DecimalField('Total valeur', places=3, validators=[InputRequired()])
pc_rdt = DecimalField('% rendement', places=2, validators=[InputRequired()])

View File

@@ -7,6 +7,7 @@ import zope.sqlalchemy
# Base.metadata prior to any initialization routines
from .user import User
from .blog_record import BlogRecord
from .portfolio import Actifs
# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
@@ -54,7 +55,7 @@ def includeme(config):
"""
Initialize the model for a Pyramid app.
Activate this setup using ``config.include('cao_blogr.models')``.
Activate this setup using ``config.include('ctp_blogr.models')``.
"""
settings = config.get_settings()

View File

@@ -1,5 +1,5 @@
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 ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
Column,
Integer,

View File

@@ -0,0 +1,63 @@
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.orm import relationship
from sqlalchemy import (
Column,
Integer,
Float,
Unicode, #<- will provide Unicode field
UnicodeText, #<- will provide Unicode text field
DateTime, #<- time abstraction field
Index,
ForeignKey,
)
class Actifs(Base):
__tablename__ = 'actifs'
no_id = Column(Integer, primary_key=True)
symbole = Column(Unicode(45), unique=True, nullable=False)
libelle = Column(Unicode(45), nullable=False)
classe = Column(Unicode(45), nullable=False)
nombre = Column(Integer)
cours = Column(Float)
pru = Column(Float)
valeur = Column(Float)
plus_value = Column(Float)
pc_plusvalue = Column(Float)
rendement = Column(Float)
pc_rdt = Column(Float)
pc_allocation = Column(Float)
ter = Column(Float)
ter_pondere = Column(Float)
devise = Column(Unicode(45), default='EUR')
parite = Column(Float)
website = Column(Unicode(100))
modif_le = Column(DateTime, default=datetime.datetime.utcnow)
__table_args__ = (Index('symbole_index', 'symbole'),)
class Allocation(Base):
__tablename__ = 'allocation'
no_cat = Column(Integer, primary_key=True)
classe = Column(Unicode(45), ForeignKey('actifs.classe'))
pc_cible = Column(Integer)
pc_atteint = Column(Float)
pc_ecart = Column(Float)
valeur = Column(Float)
type = Column(Unicode(45), default='ACTION')
ordre = Column(Integer)
bg_color = Column(Unicode(45))
__table_args__ = (Index('classe_index', 'classe'), Index('ordre_index', 'ordre'),)
class Histo(Base):
__tablename__ = 'histo'
no_id = Column(Integer, primary_key=True)
date = Column(DateTime, default=datetime.datetime.utcnow)
mvt_cash = Column(Float)
valeur_pf = Column(Float)
nb_part = Column(Float)
val_part = Column(Float)
cours_ref = Column(Float)
val_part_ref = Column(Float)

View File

@@ -1,5 +1,5 @@
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 ctp_blogr.models.meta import Base #<- we need to import our sqlalchemy metadata from which model classes will inherit
from sqlalchemy import (
Column,
Integer,

View File

@@ -3,6 +3,7 @@ def includeme(config):
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')
@@ -10,5 +11,13 @@ def includeme(config):
config.add_route('tags', '/tags')
config.add_route('tag_edit', '/tag_edit/{id}')
config.add_route('users', '/users')
config.add_route('user_add', '/user_add/{name}')
config.add_route('user_pwd', '/user_pwd/{name}')
config.add_route('user_edit', '/user_edit/{name}')
# portfolio
config.add_route('actif_edit', '/actif_edit/{no_id}')
config.add_route('actif2_edit', '/actif2_edit/{no_id}')
config.add_route('allocation_edit', '/allocation_edit/{no_cat}')
config.add_route('histo_list', '/histo_list')
config.add_route('histo_edit', '/histo_edit/{no_id}')
config.add_route('portfolio', '/portfolio')
# personal
config.add_route('portal', '/portal')

View File

@@ -12,18 +12,26 @@ class BlogRecordService(object):
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é')
query = query.filter(or_(BlogRecord.title.like(search),
BlogRecord.body.like(search))).all()
return query
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
@@ -31,7 +39,7 @@ class BlogRecordService(object):
if request.authenticated_userid == None:
# if user is anonym, display only published posts
query = query.filter(BlogRecord.status == 'publié')
query = query.order_by(sa.desc(BlogRecord.created)).limit(10).all()
query = query.order_by(sa.desc(BlogRecord.created)).limit(15).all()
return query
@classmethod

View File

@@ -0,0 +1,106 @@
import sqlalchemy as sa
from sqlalchemy import func
from ..models.portfolio import Actifs, Allocation, Histo
class PFService(object):
@classmethod
def get_actifs(cls, request, no_id):
if no_id == '0':
items = request.dbsession.query(Actifs, Allocation).join(Allocation).order_by(Allocation.ordre, Actifs.libelle).all()
else:
# lire une allocation par le no_id
items = request.dbsession.query(Actifs).filter(Actifs.no_id == no_id).first()
return items
@classmethod
def get_allocation(cls, request, no_cat):
if no_cat == '0':
query = request.dbsession.query(Allocation)
query = query.order_by(sa.asc(Allocation.ordre)).all()
else:
# lire une allocation par le no_id
query = request.dbsession.query(Allocation).filter(Allocation.no_cat == no_cat).first()
return query
@classmethod
def get_allocation_byType(cls, request, type):
# lire une allocation par le no_id
query = request.dbsession.query(Allocation).filter(Allocation.type == type).all()
return query
@classmethod
def get_histo(cls, request, no_id):
if no_id == '0':
items = request.dbsession.query(Histo).order_by(sa.asc(Histo.date)).all()
elif no_id == '-1':
items = request.dbsession.query(Histo).order_by(sa.desc(Histo.date)).all()
else:
# lire le histo par le no_id
items = request.dbsession.query(Histo).filter(Histo.no_id == no_id).first()
return items
@classmethod
def get_last_histo(cls, request):
# lire le dernier histo créé
items = request.dbsession.query(Histo).order_by(sa.desc(Histo.date)).first()
return items
@classmethod
def delete_allocation(cls, request, no_cat):
request.dbsession.query(Allocation).filter(Allocation.no_cat == no_cat).delete(synchronize_session=False)
return
@classmethod
def delete_histo(cls, request, no_id):
request.dbsession.query(Histo).filter(Histo.no_id == no_id).delete(synchronize_session=False)
return
@classmethod
def update_actif_devise(cls, request, devise, taux):
request.dbsession.query(Actifs).filter(Actifs.devise == devise).update({'parite': taux})
return
def update_actif_valeur(request, symbole, cours, valeur, plus_value, pc_plusvalue):
request.dbsession.query(Actifs).filter(Actifs.symbole == symbole
).update({'cours': cours,
'valeur': valeur,
'plus_value':plus_value,
'pc_plusvalue': pc_plusvalue,
})
return
def update_portefeuille(request, today):
# cumuler la valeur totale des actifs
result = request.dbsession.query(Actifs, func.sum(Actifs.valeur).label("TotalValue")).first()
TotalValue = result.TotalValue
# maj du pourcentage d'allocation des lignes du portefeuille
items = PFService.get_actifs(request, '0')
for item in items:
pc_allocation = round(item.Actifs.valeur / TotalValue * 100, 3)
request.dbsession.query(Actifs).filter(Actifs.no_id == item.Actifs.no_id).update({'pc_allocation': pc_allocation,
'modif_le': today})
# maj des allocations
items = PFService.get_allocation(request, '0')
for item in items:
# cumuler la valeur totale des actifs de cette classe
result = request.dbsession.query(Actifs, func.sum(Actifs.valeur).label("TotalClasse")
).filter(Actifs.classe == item.classe).first()
TotalClasse = result.TotalClasse
pc_atteint = round(TotalClasse / TotalValue * 100, 3)
# maj du pourcentage d'allocation de cette classe
request.dbsession.query(Allocation).filter(Allocation.classe == item.classe
).update({'valeur': TotalClasse,
'pc_atteint': pc_atteint})
return
def delete_actif(request, no_id):
request.dbsession.query(Actifs).filter(Actifs.no_id == no_id).delete(synchronize_session=False)
return

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -2,18 +2,18 @@
/* https://www.w3schools.com/bootstrap/bootstrap_theme_band.asp */
body {
font: 400 19px/1.8 Georgia, serif;
color: #777;
font: 100 18px/24px Times New Roman, serif;
color: rgb(90, 89, 89);
}
h3, h4 {
.container h2, h3{
color: #df4937 !important;
}
h5 {
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;
@@ -38,6 +38,21 @@ h3, h4 {
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;
@@ -57,6 +72,9 @@ h3, h4 {
border: none;
border-radius: 0;
}
.monospace-font {
font-family: Monaco, "Courier New", Courier, monospace;
}
.thumbnail p {
margin-top: 15px;
color: #555;
@@ -102,6 +120,7 @@ h3, h4 {
.navbar-default .navbar-toggle {
border-color: transparent;
}
footer {
background-color: #bc2131;
color: #f5f5f5;
@@ -141,3 +160,4 @@ textarea {
.dropdown-menu li a:hover {
background-color: red !important;
}

View File

@@ -2,7 +2,7 @@
{% block content %}
<div class="content">
<h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
<h1><span class="font-semi-bold">CTP Blog</span></h1>
<p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
</div>
{% endblock content %}

View File

@@ -1,9 +1,9 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% extends "layout.jinja2" %}
{% block content %}
{% if request.authenticated_userid %}
<p>
<a href="{{ request.route_url('home') }}">[ Retour ]</a>
<a href="{{ return_url }}">[ Retour ]</a>
<a href="{{ request.route_url('blog_edit', id=entry.id) }}">[ Modifier ]</a>
</p>
@@ -13,8 +13,7 @@
<p>{{ body_html | safe }}</p>
<hr/>
<p>
Auteur : <strong>{{ entry.author }}</strong><br>
Publié le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
Publié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
{% if request.authenticated_userid %}
Modifié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
Tag : <strong>{{ entry.tag }}</strong><br>

View File

@@ -0,0 +1,26 @@
{% extends "layout.jinja2" %}
{% block content %}
<p><a href="{{ url_retour }}">[ retour ]</a></p>
<table class="table table-condensed">
{% for entry in items %}
<tr>
<td>{{ entry.created.strftime("%d %b %y") }}</td>
<td>
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
</td>
{% if entry.status != 'publié' %}
<td><span class="label label-danger">{{ entry.status }}</span></td>
{% else %}
<td>&nbsp;</td>
{% endif%}
</tr>
{% else %}
<p class="text-danger">Aucun post trouvé !</p>
{% endfor %}
</table>
<p class="text-center">[ {{nb_items}} billets ]</p>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% extends "layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
@@ -16,7 +16,7 @@
{% endfor %}
<div class="form-group">
<label class="required-field" for="body">{{ form.body.label }}</label>
{{ form.body(class_='form-control', cols="35", rows="20") }}
{{ form.body(class_='form-control monospace-font', cols="35", rows="20") }}
</div>
<div class="form-group">
@@ -31,7 +31,9 @@
<p>
{% if blog_id != '0' %}
Créé le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong><br>
Créé le : <strong>{{ entry.created.strftime("%d-%m-%Y - %H:%M") }}</strong>
<button class="btn btn-link" type="submit" name="form.update_created">[ Mettre à jour ]</button>
<br>
Modifié le : <strong>{{ entry.edited.strftime("%d-%m-%Y - %H:%M") }}</strong>
{% endif %}
</p>

View File

@@ -1,4 +1,4 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% extends "layout.jinja2" %}
{% block content %}
<form id="search-form" class="form-horizontal" role="form" action="/blog_search" method="post">
@@ -46,6 +46,12 @@
{% endif %}
</div>
<br />
<br />
<h2 class="text-center">Index</h2>
<p class="text-center">
|&nbsp;
{% for tag in tags %}
<a href="{{ request.route_url('blog_bytag', tag=tag.tag, retour='blog_search') }}">{{ tag.tag }}</a>&nbsp;|
{% endfor %}
</p>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "layout.jinja2" %}
{% block content %}
<div>
<h3 class="text-center">{{ cartoon_title }}</h3>
<p class="text-center">
<img src="{{ cartoon_url }}"
class="img_responsive" style="width:440px" /><br>
<a href="https://www.chappatte.com/fr/" target="_blank">© Chappatte</a>
</p>
</div>
<br>
{% if request.authenticated_userid %}
<p>
<a href="{{ request.route_url('blog_edit', id='0') }}">[ Nouveau ]</a>
<a href="{{ request.route_url('blog_edit', id='95') }}">[ Sélectionner un autre le dessin ]</a>
</p>
{% endif%}
<table id="posts_list" class="table table-condensed">
{% for entry in last_ten %}
<tr>
<td>{{ entry.created.strftime("%d %b %y") }}</td>
<td>
<a href="{{ request.route_url('blog', id=entry.id, slug=entry.slug) }}">{{ entry.title }}</a>
</td>
<td>
[ <a href="{{ request.route_url('blog_bytag', tag=entry.tag, retour='home') }}">{{ entry.tag }}</a> ]
</td>
{% if entry.status != 'publié' %}
<td><span class="label label-danger">{{ entry.status }}</span></td>
{% else %}
<td>&nbsp;</td>
{% endif%}
</tr>
{% else %}
<p class="text-danger">Aucun post trouvé</p>
{% endfor %}
</table>
{% endblock %}

View File

@@ -5,7 +5,7 @@
<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')}}">
<link rel="shortcut icon" href="{{request.static_url('ctp_blogr:static/mythosaur_skull_red.ico')}}">
<title>{{page_title}}</title>
@@ -15,7 +15,7 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
<!-- Custom styles for this scaffold -->
<link href="{{request.static_url('cao_blogr:static/theme.css')}}" rel="stylesheet">
<link href="{{request.static_url('ctp_blogr:static/theme.css')}}" rel="stylesheet">
</head>
@@ -29,7 +29,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ request.route_url('home') }}">CAO Blogr</a>
<a class="navbar-brand" href="{{ request.route_url('home') }}"><img src="{{request.static_url('ctp_blogr:static/mythosaur_skull_60x150.png')}}" /></a>
</div>
<div class="collapse navbar-collapse" id="myNavbar">
<ul class="nav navbar-nav navbar-right">
@@ -42,7 +42,12 @@
<ul class="dropdown-menu">
{% if request.authenticated_userid == 'admin' %}
<li><a href="{{request.route_url('users')}}"><span class="glyphicon glyphicon-user"></span>&nbsp;&nbsp;Utilisateurs</a></li>
{% else %}
<li><a href="{{request.route_url('user_edit', name=request.authenticated_userid)}}">
<span class="glyphicon glyphicon-user"></span>&nbsp;&nbsp;Modifier le mot de passe</a>
</li>
{% endif %}
<li><a href="{{ request.route_url('portfolio') }}"><span class="glyphicon glyphicon-briefcase"></span>&nbsp;&nbsp;Portfolio</a></li>
<li><a href="{{ request.route_url('tags') }}"><span class="glyphicon glyphicon-tag"></span>&nbsp;&nbsp;Tags</a></li>
<li><a href="{{ request.route_url('logout') }}"><span class="glyphicon glyphicon-off"></span>&nbsp;&nbsp;Se déconnecter</a></li>
</ul>
@@ -58,14 +63,10 @@
</div>
</nav>
{% if request.path == '/' %}
{# -- display carousel -- #}
{% block carousel %}
{% endblock carousel %}
{% endif %}
<!-- Container (The Page Template Section) -->
<div class="container">
<br>
<br>
<!-- Display Page Title -->
{% if page_title %}
<h1>{{ page_title }}</h1>

View File

@@ -7,6 +7,8 @@
<form action="{{ login_url }}" method="post">
<h2>Se connecter</h2>
<input type="hidden" name="came_from" value="{{ came_from }}" />
<div class="form-group">
<input type="text" name="username" class="form-control" placeholder="Identifiant">
</div>
@@ -14,7 +16,7 @@
<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">
<input type="submit" name="form.submitted" value="Se connecter" class="btn btn-primary">
</div>
</form>

View File

@@ -0,0 +1,88 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
<div class="form-group">
<label class="required-field">{{ form.classe.label }}</label>
{{ form.classe(class_='form-control') }}
</div>
{% for error in form.symbole.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="symbole">{{form.symbole.label}}</label>
{{form.symbole(class_='form-control')}}
</div>
{% for error in form.libelle.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="libelle">{{form.libelle.label}}</label>
{{form.libelle(class_='form-control')}}
</div>
<div class="form-group">
<label class="required-field" for="pru">{{form.pru.label}}</label>
<div class="input-group">
<div class="input-group-addon">{{ item.devise }}</div>
{{form.pru(class_='form-control')}}
</div>
</div>
<div class="form-group">
<label class="required-field" for="cours">{{form.cours.label}}</label>
<div class="input-group">
<div class="input-group-addon">{{ item.devise }}</div>
{{form.cours(class_='form-control')}}
</div>
</div>
<div class="form-group">
<p class="form-control-static"><b>Dernière modif. :</b> {{ item.modif_le }}</p>
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('portfolio') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.no_id.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cette allocation</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.symbole.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,111 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
<div class="form-group">
<label class="required-field">{{ form.classe.label }}</label>
{{ form.classe(class_='form-control') }}
</div>
{% for error in form.symbole.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="symbole">{{form.symbole.label}}</label>
{{form.symbole(class_='form-control')}}
</div>
{% for error in form.libelle.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="libelle">{{form.libelle.label}}</label>
{{form.libelle(class_='form-control')}}
</div>
<div class="form-group">
<label class="required-field" for="nombre">{{form.nombre.label}}</label>
{{form.nombre(class_='form-control')}}
</div>
<div class="form-group">
<label class="control-label required-field" for="pru">{{form.pru.label}}</label>
<div class="input-group">
<div class="input-group-addon">{{ item.devise }}</div>
{{form.pru(class_='form-control')}}
</div>
</div>
<div class="form-group">
<label class="required-field" for="ter">{{form.ter.label}}</label>
<div class="input-group">
<div class="input-group-addon">%</div>
{{form.ter(class_='form-control')}}
</div>
</div>
<div class="form-group">
<label class="required-field" for="pc_rdt">{{form.pc_rdt.label}}</label>
<div class="input-group">
<div class="input-group-addon">%</div>
{{form.pc_rdt(class_='form-control')}}
</div>
</div>
<div class="form-group">
<label class="required-field" for="website">{{form.website.label}}</label>
<div class="input-group">
<div class="input-group-addon">
<a href="{{ item.website }}" target="_blank"><span class="glyphicon glyphicon-globe"></span></a>
</div>
{{form.website(class_='form-control')}}
</div>
</div>
<div class="form-group">
<p class="form-control-static"><b>Dernière modif. :</b> {{ item.modif_le }}</p>
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('portfolio') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.no_id.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cette allocation</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.symbole.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
<div class="form-group">
<label class="required-field">{{ form.classe.label }}</label>
{{ form.classe(class_='form-control') }}
</div>
{% for error in form.pc_cible.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="tag">{{form.pc_cible.label}}</label>
{{form.pc_cible(class_='form-control')}}
</div>
<div class="form-group">
<label class="required-field" for="tag">{{form.type.label}}</label>
{{form.type(class_='form-control')}}
</div>
<div class="form-group">
<label class="required-field" for="tag">{{form.bg_color.label}}</label>
{{form.bg_color(class_='form-control')}}
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('portfolio') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.no_cat.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cette allocation</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.classe.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,64 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<form action="{{ url }}" method="post" class="form">
<div class="form-group">
<p class="form-control-static">Date : <b>{{ item.date.strftime('%d-%m-%Y') }}</b></p>
</div>
{% for error in form.mvt_cash.errors %}
<div class="error">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="tag">{{form.mvt_cash.label}}</label>
<div class="input-group">
<div class="input-group-addon">€</div>
{{form.mvt_cash(class_='form-control')}}
</div>
<p class="form-control-static">(si le montant est négatif, le montant est retiré du portefeuille)</b></p>
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ request.route_url('histo_list') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
{% if form.no_id.data %}
<button class="btn btn-danger" type="button" data-toggle="modal" data-target="#confirmDelete">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% else %}
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% endif %}
</div>
</form>
<!-- Modal : Confirmation SUPRESSION -->
<div id="confirmDelete" class="modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Supprimer cet historique</h4>
</div>
<div class="modal-body">
<!-- The form is placed inside the body of modal -->
<p>Etes-vous certain(e) de vouloir supprimer la ligne <b>{{ form.no_id.data }}</b> ?</p>
</div>
<div class="modal-footer">
<div class="form-group">
<div class="text-center">
<form id="confirmForm" method="post">
<button type="submit" class="btn btn-danger" name="form.deleted">Supprimer</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,49 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<p>
<a class="btn btn-default" href="{{ request.route_url('portfolio') }}">
<span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<a href="{{ request.route_url('histo_edit', no_id='0') }}" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-plus"></span> Entrée / Sortie cash</a>
</p>
<table id="histo_list" class="table table-striped table-bordered table-condensed">
<thead>
<tr>
<th>Date</th>
<th align='right'>E/S Cash</th>
<th align='right'>Valeur Pf</th>
<th align='right'>Nb Part</th>
<th align='right'>Valeur Part</th>
<th align='right'>Cours ref</th>
<th align='right'>Nb Part ref</th>
<th align='center'>No Id</th>
</tr>
</thead>
{% for item in items %}
<tr>
<td>{{ item.date.strftime('%d/%m/%Y') }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.mvt_cash) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.valeur_pf) }}</td>
<td align='right'>{{ '{0:0.2f}'.format(item.nb_part) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.val_part) }}</td>
<td align='right'>{{ '{0:0.2f} €'.format(item.cours_ref) }}</td>
<td align='right'>{{ '{0:0.2f}'.format(item.val_part_ref) }}</td>
<td align='center'>
<a href="{{ request.route_url('histo_edit', no_id=item.no_id) }}">{{ item.no_id }}</a>
</td>
</tr>
{% endfor %}
</table>
<br />
<br />
{% endblock %}

View File

@@ -0,0 +1,245 @@
{% extends "ctp_blogr:templates/layout.jinja2" %}
{% block content %}
<p>
<a href="allocation_edit/0">[ Nouvelle classe ]</a>
</p>
<div class="row">
<div class="col-md-6">
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th>Classe</th>
<th class="text-right">% cible</th>
<th class="text-right">% actuel</th>
<th class="text-right">Ecart</th>
<th class="text-right">Valeur</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td class="{{ item.bg_color }}"><a href="allocation_edit/{{ item.no_cat }}">{{ item.classe }}</a></td>
<td class="text-right">{{ item.pc_cible }} %</td>
<td class="text-right">{{ '{0:0.1f}'.format(item.pc_atteint) }} %</td>
{% if (item.pc_atteint - item.pc_cible) >= 0 %}
<td class="text-right" style="color: green;">{{ '{0:0.1f}'.format(item.pc_ecart) }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ '{0:0.1f}'.format(item.pc_ecart) }}</td>
{% endif %}
<td class="text-right">{{ '{0:0,.0f} €'.format(item.valeur).replace(',',' ') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-6">
<table id="portfolio" class="table table-condensed table-bordered">
<thead>
<tr>
<th>Portefeuille</th>
<th class="text-right">Montant</th>
<th class="text-right">%</th>
</tr>
</thead>
<tbody>
<tr>
<td>Valorisation</td>
<td class="text-right">{{ '{0:0.2f} €'.format(total_valeur).replace(',',' ') }}</td>
<td></td>
</tr>
<tr>
<td>Plus value</td>
<td class="text-right">{{ '{0:0,.0f} €'.format(total_pv).replace(',',' ') }}</td>
<td class="text-right">{{ '{0:0.1f} %'.format(total_pc_value) }}</td>
</tr>
<tr>
<td>Safe Withdrawal Rate</td>
<td class="text-right text-success"><b>{{ '{0:0,.0f} €'.format(swr_amount) }}</b></td>
<td class="text-right text-success"><b>{{ '{0:0.1f} %'.format(swr_rate) }}</b></td>
</tr>
</tbody>
</table>
<p>
<b>Allocation globale</b> : 70% actions + 30% obligations<br />
[Inspirée du
<a href="https://www.nbim.no/en/the-fund/how-we-invest/benchmark-index/" target="_blank">Fond souverain Norvégien</a>]<br />
<b>Allocation actions</b> : 80% Monde (56%) + 20% Croissance (14%)<br />
</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<!-- graphique donut cible -->
<div id="donutchart_cible" style="width: 100%; height: 500px;"></div>
</div>
<div class="col-md-6">
<!-- graphique donut actuel -->
<div id="donutchart_actuel" style="width: 100%; height: 500px;"></div>
</div>
</div>
<div class="row">
<h2>Mes actifs</h2>
<p>"<i>Diversification is not determined by the number of securities held.</i>"
<a href="http://www.etf.com/sections/index-investor-corner" target="_blank">Larry Swedroe</a></p>
<form id="actif_list-form" action="{{ url }}" method="post">
<div class="form-group">
<button id="updateButton" class="btn btn-primary" type="submit" name="form.submitted">
<i class="glyphicon glyphicon-refresh"></i> MAJ du portefeuille</button>
<a href="#" data-toggle="modal" data-target="#choixTypeActif">[ Nouvel actif ]</a>
<a href="/histo_list">[ Historique ]</a>
<a href="/blog/2/mouvements-du-portefeuille">[ Mouvements ]</a>
</div>
</form>
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th>Classe</th>
<th>Libellé</th>
<th class="text-right">Cours</th>
<th class="text-right">Nb</th>
<th class="text-right">Valeur</th>
<th class="text-right">+/- Valeur</th>
<th class="text-right">% de +/-</th>
<th class="text-right">% TER</th>
<th class="text-right">% PF</th>
</tr>
</thead>
<tbody>
{% for ligne in actifs %}
<tr>
<td class="{{ ligne.Allocation.bg_color }}">{{ ligne.Allocation.classe }}</td>
{% if ligne.Allocation.type=='ACTION' %}
<td><a href="actif_edit/{{ ligne.Actifs.no_id }}">{{ ligne.Actifs.libelle }}</a></td>
{% else %}
<td><a href="actif2_edit/{{ ligne.Actifs.no_id }}">{{ ligne.Actifs.libelle }}</a></td>
{% endif %}
{% if ligne.Actifs.devise=='EUR' %}
<td class="text-right">{{ '{0:0.2f} €'.format(ligne.Actifs.cours) }}</td>
{% else %}
<td class="text-right">{{ '{0:0.2f} $'.format(ligne.Actifs.cours) }}</td>
{% endif %}
<td class="text-right">{{ ligne.Actifs.nombre }}</td>
<td class="text-right">{{ '{0:0,.2f} €'.format(ligne.Actifs.valeur).replace(',',' ') }}</td>
{% if ligne.Actifs.plus_value >= 0 %}
<td class="text-right" style="color: green;">{{ '{0:0,.2f} €'.format(ligne.Actifs.plus_value).replace(',',' ') }}</td>
<td class="text-right" style="color: green;">{{ '{0:0.1f}'.format(ligne.Actifs.pc_plusvalue) }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ '{0:0,.2f} €'.format(ligne.Actifs.plus_value).replace(',',' ') }}</td>
<td class="text-right" style="color: red;">{{ '{0:0.1f}'.format(ligne.Actifs.pc_plusvalue) }}</td>
{% endif %}
<td class="text-right">{{ '{0:0.1f}'.format(ligne.Actifs.ter) }}</td>
<td class="text-right">{{ '{0:0.1f}'.format(ligne.Actifs.pc_allocation) }}</td>
</tr>
{% endfor %}
<tr>
<td class="text-right" colspan="4"><b>Total au {{ maj_pf_le.strftime('%d/%m/%Y') }}</b></td>
<td>{{ '{0:0,.2f} €'.format(total_valeur).replace(',',' ') }}</td>
{% if total_pv >= 0 %}
<td class="text-right" style="color: green;">{{ '{0:0,.2f} €'.format(total_pv).replace(',',' ') }}</td>
<td class="text-right" style="color: green;">{{ '{0:0.1f}'.format(total_pc_value) }}</td>
{% else %}
<td class="text-right" style="color: red;">{{ '{0:0,.2f} €'.format(total_pv).replace(',',' ') }}</td>
<td class="text-right" style="color: red;">{{ '{0:0.1f}'.format(total_pc_value) }}</td>
{% endif %}
<td></td>
<td class="text-right"><b>100.0</b></td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<div>
<!-- graphique evolution -->
<div id="curve_chart" style="width: 100%; height: 500px;"></div>
</div>
</div>
<!-- MODAL POPUP : Affichage de la FAQ -->
<div class="modal fade" id="choixTypeActif" tabindex="-1" role="dialog" aria-labelledby="choixTypeActif" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span class="glyphicon glyphicon-remove"></span></button>
<h4 class="modal-title" id="choixTypeActif">Choix du type</h4>
</div>
<div class="modal-body">
<p>
Voulez-vous créer un actif de type <br />
<br />
<a href="/actif_edit/0" class="btn btn-primary" role="button">
ACTION</a> ou
<a href="/actif2_edit/0" class="btn btn-warning" role="button">
AUTRE</a>
<br />
<br />
<p>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$('#updateButton').on('click', function(){
$('i.gly-spin').removeClass('gly-spin');
$('i').addClass('gly-spin');
});
</script>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load("current", {packages:["corechart"]});
google.charts.setOnLoadCallback(drawChart);
var dataSet_cible = {{ donut_cible | safe }};
var dataSet_actuel = {{ donut_actuel | safe }};
var dataSet_evoln = {{ courbe_evoln | safe }};
function drawChart() {
var data_cible = google.visualization.arrayToDataTable(dataSet_cible);
var data_actuel = google.visualization.arrayToDataTable(dataSet_actuel);
var data_evoln = google.visualization.arrayToDataTable(dataSet_evoln);
var options_cible = {
title: 'Allocation cible',
pieHole: 0.4,
slices: {
0: {color: 'SteelBlue'}, 1: {color: 'LightSteelBlue'},
2: {color: 'Maroon'}, 3: {color: 'Brown'},
5: {offset: 0.2, color: 'DarkGreen'}, 6: {offset: 0.3, color: 'Green'},
},
};
var options_actuel = {
title: 'Allocation actuelle',
pieHole: 0.4,
slices: {
0: {color: 'SteelBlue'}, 1: {color: 'LightSteelBlue'},
2: {color: 'Maroon'}, 3: {color: 'Brown'},
5: {offset: 0.2, color: 'DarkGreen'}, 6: {offset: 0.3, color: 'Green'},
},
};
var options_evoln = {
title: 'Evolution du portefeuille VS fond Carmignac Investissement',
curveType: 'function',
legend: { position: 'top' }
};
var chart_cible = new google.visualization.PieChart(document.getElementById('donutchart_cible'));
chart_cible.draw(data_cible, options_cible);
var chart_actuel = new google.visualization.PieChart(document.getElementById('donutchart_actuel'));
chart_actuel.draw(data_actuel, options_actuel);
var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
chart.draw(data_evoln, options_evoln);
}
</script>
</div><!-- content -->
</div>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% extends "layout.jinja2" %}
{% block content %}

View File

@@ -1,4 +1,4 @@
{% extends "cao_blogr:templates/layout.jinja2" %}
{% extends "layout.jinja2" %}
{% block content %}

View File

@@ -0,0 +1,55 @@
{% extends "layout.jinja2" %}
{% block content %}
{% if message %}
<div class="alert alert-danger">
{{ message }}
</div>
{% endif %}
<form action="{{ url }}" method="post" class="form">
{% for error in form.name.errors %}
<div class="label label-warning">{{ error }}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="name">{{form.name.label}}</label>
{% if form.id.data %}
<input class="form-control" name="name" readonly type="text" value="{{form.name.data}}">
{% else %}
{{form.name(class_='form-control')}}
{% endif %}
</div>
<div class="form-group">
<label class="required-field" for="password">{{form.password.label}}</label>
{{form.password(class_='form-control')}}
</div>
{% for error in form.confirm.errors %}
<div class="label label-danger">{{error}}</div>
{% endfor %}
<div class="form-group">
<label class="required-field" for="confirm">{{form.confirm.label}}</label>
{{form.confirm(class_='form-control')}}
</div>
<br>
<div class="form-group">
<a class="btn btn-default" href="{{ url_retour }}"><span class="glyphicon glyphicon-chevron-left"></span> Retour</a>
<button class="btn btn-primary" type="submit" name="form.submitted">
<span class="glyphicon glyphicon-ok"></span> Enregistrer</button>
{% if form.id.data and request.authenticated_userid == 'admin' %}
<button class="btn btn-warning" type="submit" name="form.deleted">
<span class="glyphicon glyphicon-remove"></span> Supprimer</button>
{% endif %}
</div>
</form>
{% endblock %}

View File

@@ -4,7 +4,7 @@
<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">
<a href="{{ request.route_url('user_edit', name='0') }}" class="btn btn-success" role="button">
<span class="glyphicon glyphicon-plus"></span> Nouvel utilisateur</a>
</p>
@@ -20,7 +20,7 @@
<tr>
<td>{{ entry.id }}</td>
<td>
<a href="{{ request.route_url('user_pwd', name=entry.name) }}">
<a href="{{ request.route_url('user_edit', name=entry.name) }}">
{{ entry.name }}
</a>
</td>

View File

@@ -6,8 +6,7 @@ from ..forms import BlogCreateForm, BlogUpdateForm, BlogSearchForm, TagForm
import markdown
import datetime #<- will be used to set default dates on models
@view_config(route_name='blog',
renderer='cao_blogr:templates/blog.jinja2')
@view_config(route_name='blog', renderer='ctp_blogr:templates/blog.jinja2')
def blog(request):
# get post id from request
blog_id = request.matchdict['id']
@@ -22,14 +21,21 @@ def blog(request):
# convertir de markdown en HTML
body_html = markdown.markdown(body, extensions=['footnotes'])
# si page mouvement de portfolio
if blog_id == '2':
return_url = "/portfolio"
else:
return_url = "/"
return {
'page_title': entry.title,
'entry': entry,
'body_html': body_html,
'return_url': return_url,
}
@view_config(route_name='blog_edit', renderer='cao_blogr:templates/blog_edit.jinja2', permission='view')
@view_config(route_name='blog_edit', renderer='ctp_blogr:templates/blog_edit.jinja2', permission='view')
def blog_edit(request):
# get post id from request
blog_id = request.matchdict['id']
@@ -74,6 +80,12 @@ def blog_edit(request):
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')
@@ -86,12 +98,32 @@ def blog_edit(request):
'blog_id': blog_id,
'entry': entry, }
@view_config(route_name='blog_bytag', renderer='ctp_blogr: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)
@view_config(route_name='blog_search',
renderer='cao_blogr:templates/blog_search.jinja2')
# 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='ctp_blogr: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():
@@ -104,12 +136,13 @@ def blog_search(request):
'form': form,
'items': items,
'criteria': criteria,
'tags': tags,
}
@view_config(route_name='tags', renderer='cao_blogr:templates/tags.jinja2', permission='view')
@view_config(route_name='tags', renderer='ctp_blogr:templates/tags.jinja2', permission='view')
def tags(request):
# get the list of tags of this topic
# get the list of tags
tags = BlogRecordService.get_tags(request)
return {
@@ -117,7 +150,7 @@ def tags(request):
'tags': tags,
}
@view_config(route_name='tag_edit', renderer='cao_blogr:templates/tag_edit.jinja2', permission='view')
@view_config(route_name='tag_edit', renderer='ctp_blogr:templates/tag_edit.jinja2', permission='view')
def tag_edit(request):
# get tag parameters from request
tag_id = request.matchdict['id']

160
ctp_blogr/views/default.py Normal file
View File

@@ -0,0 +1,160 @@
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,
}
@view_config(route_name='portal', renderer='../templates/portal.jinja2')
def portal(request):
return {
'page_title': "A propos",
}

View File

@@ -0,0 +1,373 @@
from pyramid.view import (
view_config,
)
from pyramid.httpexceptions import HTTPFound
from ..services.portfolio import PFService
from ..forms import ActifForm, Actif2Form, AllocationForm, HistoForm
from ..models.portfolio import Actifs, Histo, Allocation
import datetime #<- will be used to set default dates on models
import yfinance as yf
import json
import html
@view_config(route_name='portfolio', renderer='../templates/portfolio/portfolio.jinja2', permission='view')
def portfolio(request):
logged_in = request.authenticated_userid
url = request.route_url('portfolio')
# lire les categories
items = PFService.get_allocation(request, '0')
# construire la liste pour donut
donut_cible=[]
donut_cible.append(('Allocation cible', 'Pourcent'))
donut_actuel=[]
donut_actuel.append(('Allocation actuelle', 'Pourcent'))
# calculer % total
total = 0
for item in items:
# construire la liste pour donut cible
d = (item.classe, item.pc_cible)
donut_cible.append(d)
# construire la liste pour donut actuel
d = (item.classe, int(item.pc_atteint * 10))
donut_actuel.append(d)
# totaliser les pourcentages
total += item.pc_cible
swr_rate = 3.5
if total != 100:
request.session.flash('Attention, le total de votre répartition cible dépasse 100% : ' + str(total), 'warning')
# lire les actifs
actifs = PFService.get_actifs(request, '0')
# MAJ du prtefeuille
if 'form.submitted' in request.params:
# lire le cours de EURUSD
ticker = yf.Ticker('EUR=X')
# maj des parités des devises
taux = ticker.fast_info.get('lastPrice')
PFService.update_actif_devise(request, 'USD', taux)
for item in actifs:
if item.Allocation.type == 'ACTION':
symbole = item.Actifs.symbole
# lire le cours de l'action
ticker = yf.Ticker(symbole)
# ticker delisted ?
if symbole == 'SHLDQ':
cours = 0.1
else:
cours = round(ticker.fast_info.get('lastPrice'), 3)
valeur = round(cours * item.Actifs.parite * item.Actifs.nombre, 3)
plus_value = round(valeur - (item.Actifs.pru * item.Actifs.nombre),2)
pc_plusvalue = round(valeur * 100 / (item.Actifs.pru * item.Actifs.nombre) - 100, 3)
PFService.update_actif_valeur(request, symbole, cours, valeur, plus_value, pc_plusvalue)
# time.sleep(1) # attendre 2 secondes
# update du portefeuille
today = datetime.datetime.now()
PFService.update_portefeuille(request, today)
# relire les actifs
actifs = PFService.get_actifs(request, '0')
request.session.flash('Le portefeuille est mis à jour avec succès.', 'success')
total_valeur = 0
total_pv = 0
total_rdt = 0
maj_pf_le = None
for item in actifs:
maj_pf_le = item.Actifs.modif_le
total_valeur += item.Actifs.valeur
total_pv += item.Actifs.plus_value
if total_valeur == 0:
total_pc_value = 0
else:
total_pc_value = total_pv / total_valeur * 100
total_rdt += item.Actifs.rendement
# lire l'historique
histos = PFService.get_histo(request,'0')
courbe_evoln=[]
courbe_evoln.append(('Date', 'Valeur part PF', 'Valeur part CARINVT:FP'))
for item in histos:
# construire la liste pour donut cible
d = (item.date.strftime('%d/%m/%Y'), int(item.val_part * 1000), int(item.val_part_ref * 1000))
courbe_evoln.append(d)
return {
'page_title': "Portefeuille",
'url': url,
'items': items,
'donut_cible': json.dumps(donut_cible),
'donut_actuel': json.dumps(donut_actuel),
'courbe_evoln': json.dumps(courbe_evoln),
'actifs': actifs,
'total_valeur': total_valeur,
'total_pv': total_pv,
'total_pc_value': total_pc_value,
'total_rdt': total_rdt,
'maj_pf_le' : maj_pf_le,
'swr_rate' : swr_rate,
'swr_amount': float(total_valeur) * swr_rate / 100,
}
@view_config(route_name='histo_list', renderer='../templates/portfolio/histo_list.jinja2', permission='view')
def histo_list(request):
# lire l historique
items = PFService.get_histo(request, '-1')
return {
'page_title': 'Historique des parts',
'items': items,
}
@view_config(route_name='allocation_edit', renderer='../templates/portfolio/allocation_edit.jinja2', permission='view')
def allocation_edit(request):
no_cat = request.matchdict['no_cat']
url = request.route_url('allocation_edit', no_cat=no_cat)
if no_cat == '0':
# create a new allocation
entry = Allocation()
form = AllocationForm(request.POST, entry)
page_title = "Nouvelle allocation"
else:
# modify post
entry = PFService.get_allocation(request, no_cat)
if not entry:
request.session.flash(u"Allocation non trouvée : %s" % no_cat, 'warning')
return HTTPFound(location=request.route_url('portfolio'))
form = AllocationForm(request.POST, entry)
page_title = "Modifier Allocation : " + str(entry.no_cat)
if 'form.submitted' in request.params :
if no_cat == '0':
form.populate_obj(entry)
entry.pc_atteint = 0
entry.pc_ecart = 0
entry.valeur = 0
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('portfolio'))
else:
del form.no_cat # SECURITY: prevent overwriting of primary key
form.populate_obj(entry)
return HTTPFound(location=request.route_url('portfolio'))
if 'form.deleted' in request.params:
PFService.delete_allocation(request, entry.no_cat)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('portfolio'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}
@view_config(route_name='histo_edit', renderer='../templates/portfolio/histo_edit.jinja2', permission='view')
def histo_edit(request):
no_id = request.matchdict['no_id']
url = request.route_url('histo_edit', no_id=no_id)
if no_id == '0':
# create a new tag
entry = Histo()
entry.mvt_cash = 0
entry.date = datetime.datetime.now()
form = HistoForm(request.POST, entry)
page_title = "Nouveau Histo"
else:
# modify post
entry = PFService.get_histo(request, no_id)
if not entry:
request.session.flash(u"Histo non trouvé : %s" % no_id, 'warning')
return HTTPFound(location=request.route_url('histo_list'))
form = HistoForm(request.POST, entry)
page_title = "Modifier histo no " + str(entry.no_id)
if 'form.submitted' in request.params and form.validate():
form.populate_obj(entry)
# lire le cours de l'indice de réfence : Carmignac Investissement A EUR Acc
ticker = yf.Ticker('0P00000FB2.F')
entry.cours_ref = round(ticker.fast_info.last_price, 3)
# lire le dernier histo
last = PFService.get_last_histo(request)
# lire les actifs et cumuler leurs valeurs
actifs = PFService.get_actifs(request, '0')
valeur_pf = 0
for item in actifs:
valeur_pf += item.Actifs.valeur
entry.valeur_pf = valeur_pf
# nlle valeur part = ancienne + nouvelle ratio
entry.nb_part = round(last.nb_part + (float(entry.mvt_cash) / (valeur_pf - float(entry.mvt_cash))/last.nb_part), 3)
entry.val_part = round(entry.valeur_pf / entry.nb_part, 3)
entry.val_part_ref = round(float(entry.cours_ref) * last.val_part_ref / last.cours_ref, 3)
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('histo_list'))
if 'form.deleted' in request.params:
PFService.delete_histo(request, entry.no_id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('histo_list'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}
@view_config(route_name='actif_edit', renderer='../templates/portfolio/actif_edit.jinja2', permission='view')
def actif_edit(request):
no_id = request.matchdict['no_id']
url = request.route_url('actif_edit', no_id=no_id)
# get the list of classes
classes = PFService.get_allocation_byType(request, 'ACTION')
if no_id == '0':
# create a new allocation
entry = Actifs()
entry.pru = 0
entry.cours = 0
entry.pc_rdt = 0
entry.ter = 0
form = ActifForm(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes]
page_title = "Nouvel actif"
else:
# modify post
entry = PFService.get_actifs(request, no_id)
if not entry:
request.session.flash(u"Actif non trouvé : %s" % no_id, 'warning')
return HTTPFound(location=request.route_url('portfolio'))
form = ActifForm(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes]
page_title = "Modifier Actif : " + entry.symbole
if 'form.submitted' in request.params :
if no_id == '0':
form.populate_obj(entry)
# récupérer le cours du symbole de Yahoo finance
ticker = yf.Ticker(entry.symbole)
entry.cours = ticker.fast_info.get('lastPrice')
entry.devise = ticker.fast_info.get('currency')
# raccourcir le libelle
entry.pc_allocation = 1.0
entry.valeur = round(float(entry.cours) * entry.parite * entry.nombre, 3)
entry.plus_value = round(entry.valeur - float(entry.pru * entry.nombre), 3)
entry.pc_plusvalue = round(entry.plus_value / entry.valeur * 100, 3)
entry.rendement = 0 # round(entry.valeur * float(entry.pc_rdt) / 100, 3)
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('portfolio'))
else:
if entry.symbole != 'SHLDQ':
del form.no_id # SECURITY: prevent overwriting of primary key
form.populate_obj(entry)
# récupérer le cours du symbole de Yahoo finance
ticker = yf.Ticker(entry.symbole)
entry.cours = ticker.fast_info.get('lastPrice')
entry.devise = ticker.fast_info.get('currency')
# raccourcir le libelle
entry.valeur = round(float(entry.cours) * entry.parite * entry.nombre, 3)
entry.plus_value = round(entry.valeur - float(entry.pru * entry.nombre), 3)
entry.pc_plusvalue = round(entry.plus_value / entry.valeur * 100, 3)
entry.rendement = 0 # round(entry.valeur * float(entry.pc_rdt) / 100, 3)
return HTTPFound(location=request.route_url('portfolio'))
if 'form.deleted' in request.params:
PFService.delete_actif(request, entry.no_id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('portfolio'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}
@view_config(route_name='actif2_edit', renderer='../templates/portfolio/actif2_edit.jinja2', permission='view')
def actif2_edit(request):
no_id = request.matchdict['no_id']
url = request.route_url('actif2_edit', no_id=no_id)
# get the list of classes
classes = PFService.get_allocation_byType(request, 'AUTRE')
if no_id == '0':
# create a new allocation
entry = Actifs()
entry.pru = 0
entry.cours = 0
entry.pc_rdt = 0
entry.ter = 0
entry.devise = 'EUR'
form = Actif2Form(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes]
page_title = "Nouvel actif"
else:
# modify post
entry = PFService.get_actifs(request, no_id)
if not entry:
request.session.flash(u"Actif non trouvé : %s" % no_id, 'warning')
return HTTPFound(location=request.route_url('portfolio'))
form = Actif2Form(request.POST, entry)
form.classe.choices = [(row.classe, row.classe) for row in classes]
page_title = "Modifier Actif : " + entry.symbole
if 'form.submitted' in request.params :
if no_id == '0':
form.populate_obj(entry)
entry.nombre = 1000
entry.devise = 'EUR'
entry.parite = 1.0
entry.pc_allocation = 1.0
entry.valeur = float(entry.cours) * entry.parite * entry.nombre;
entry.plus_value = entry.valeur - float(entry.pru * entry.nombre);
entry.pc_plusvalue = entry.plus_value / entry.valeur * 100;
entry.rendement = entry.valeur * float(entry.pc_rdt) / 100;
request.dbsession.add(entry)
return HTTPFound(location=request.route_url('portfolio'))
else:
del form.no_id # SECURITY: prevent overwriting of primary key
form.populate_obj(entry)
entry.valeur = float(entry.cours) * entry.parite * entry.nombre;
entry.plus_value = entry.valeur - float(entry.pru * entry.nombre);
entry.pc_plusvalue = entry.plus_value / entry.valeur * 100;
entry.rendement = entry.valeur * float(entry.pc_rdt) / 100;
return HTTPFound(location=request.route_url('portfolio'))
if 'form.deleted' in request.params:
PFService.delete_actif(request, entry.no_id)
request.session.flash("La fiche a été supprimée avec succès.", 'success')
return HTTPFound(location=request.route_url('portfolio'))
return {
'page_title': page_title,
'url': url,
'form': form,
'item': entry,
}

View File

@@ -4,7 +4,7 @@
###
[app:main]
use = egg:cao_blogr
use = egg:ctp_blogr
pyramid.reload_templates = true
pyramid.debug_authorization = false
@@ -14,7 +14,7 @@ pyramid.default_locale_name = en
pyramid.includes =
pyramid_debugtoolbar
sqlalchemy.url = sqlite:///%(here)s/cao_blogr.sqlite
sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite
retry.attempts = 3
@@ -23,7 +23,7 @@ retry.attempts = 3
# debugtoolbar.hosts = 127.0.0.1 ::1
[pshell]
setup = cao_blogr.pshell.setup
setup = ctp_blogr.pshell.setup
###
# wsgi server configuration
@@ -31,13 +31,13 @@ setup = cao_blogr.pshell.setup
[alembic]
# path to migration scripts
script_location = cao_blogr/alembic
script_location = ctp_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
listen = localhost:9280
###
# logging configuration
@@ -45,7 +45,7 @@ listen = localhost:6543
###
[loggers]
keys = root, cao_blogr, sqlalchemy
keys = root, ctp_blogr, sqlalchemy
[handlers]
keys = console
@@ -57,10 +57,10 @@ keys = generic
level = INFO
handlers = console
[logger_cao_blogr]
[logger_ctp_blogr]
level = DEBUG
handlers =
qualname = cao_blogr
qualname = ctp_blogr
[logger_sqlalchemy]
level = WARN

50
email_avnes_purge.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: utf8 -*-
#
# Compter les emails BEFORE DATE
#
from pprint import pprint
import datetime
import imaplib
# connecter au serveur IMAP
conn = imaplib.IMAP4_SSL('mail.gandi.net')
conn.login('ytranhong@free.fr', 'minhy116')
# lister les dossiers
typ, data = conn.list()
print('Liste des dossiers :')
pprint(data)
# delete mails before 14 years
before_date = (datetime.date.today() - datetime.timedelta(365.25 * 13)).strftime("%d-%b-%Y")
print("Delete emails before " + before_date)
# select ALL
conn.select('INBOX')
rv, data = conn.search(None, '(BEFORE {0})'.format(before_date))
nb_mails = str(len(data[0]))
print(nb_mails + " emails founded")
resp = input ("Enter 'c' to continue, or 'a' to abort : ")
if resp=="c":
print("Moving " + nb_mails + " emails to Trash")
messages = data[0].split(b' ')
for mail in messages:
# move to trash
conn.store(mail, '+X-GM-LABELS', '\\Trash')
#This block empties trash, remove if you want to keep, Gmail auto purges trash after 30 days.
print("Emptying Trash & Expunge...")
conn.select('[Gmail]/Corbeille')
conn.store("1:*", '+FLAGS', '\\Deleted')
# delete all the selected messages
conn.expunge()
print("Script completed")
else:
print("Script aborted")
# deconnexion du serveur
conn.close()
conn.logout()

50
gmail_caotek_purge.py Normal file
View File

@@ -0,0 +1,50 @@
# -*- coding: utf8 -*-
#
# Compter les emails BEFORE DATE
#
from pprint import pprint
import datetime
import imaplib
# connecter au serveur IMAP
conn = imaplib.IMAP4_SSL('imap.gmail.com')
conn.login('phuoc@caotek.fr', 'pcao.8211')
# lister les dossiers
typ, data = conn.list()
print('Liste des dossiers :')
pprint(data)
# delete mails before 14 years
before_date = (datetime.date.today() - datetime.timedelta(365.25 * 13)).strftime("%d-%b-%Y")
print("Delete emails before " + before_date)
# select ALL
conn.select('INBOX')
rv, data = conn.search(None, '(BEFORE {0})'.format(before_date))
nb_mails = str(len(data[0]))
print(nb_mails + " emails founded")
resp = input ("Enter 'c' to continue, or 'a' to abort : ")
if resp=="c":
print("Moving " + nb_mails + " emails to Trash")
messages = data[0].split(b' ')
for mail in messages:
# move to trash
conn.store(mail, '+X-GM-LABELS', '\\Trash')
#This block empties trash, remove if you want to keep, Gmail auto purges trash after 30 days.
print("Emptying Trash & Expunge...")
conn.select('[Gmail]/Corbeille')
conn.store("1:*", '+FLAGS', '\\Deleted')
# delete all the selected messages
conn.expunge()
print("Script completed")
else:
print("Script aborted")
# deconnexion du serveur
conn.close()
conn.logout()

53
gmail_ctphuoc_purge.py Normal file
View File

@@ -0,0 +1,53 @@
# -*- coding: utf8 -*-
#
# Compter les emails BEFORE DATE et les supprimer
#
from pprint import pprint
import datetime
import imaplib
# connecter au serveur IMAP
# Gmail avec authentification à deux facteurs (2FA),
# demander un mot de passe spécifique du compte Gmail
conn = imaplib.IMAP4_SSL('imap.gmail.com')
conn.login('ctphuoc@gmail.com', 'ztwciswzhxxogcfv')
# lister les dossiers
typ, data = conn.list()
print('Liste des dossiers :')
pprint(data)
# supprimer les mails au-dela de 14 ans
before_date = (datetime.date.today() - datetime.timedelta(365.25 * 13)).strftime("%d-%b-%Y")
print("Supprimer les emails avant " + before_date)
# select ALL
conn.select('INBOX')
rv, data = conn.search(None, '(BEFORE {0})'.format(before_date))
nb_mails = str(len(data[0]))
print(nb_mails + " emails trouvés")
resp = input ("Entrer 'c' pour continuer, or 'a' to annuler : ")
if resp=="c":
print("Déplacer " + nb_mails + " emails à la Corbeille")
messages = data[0].split(b' ')
for mail in messages:
# move to trash
conn.store(mail, '+X-GM-LABELS', '\\Trash')
#This block empties trash, remove if you want to keep, Gmail auto purges trash after 30 days.
print("Vider la Corbeille & Expurger...")
conn.select('[Gmail]/Corbeille')
conn.store("1:*", '+FLAGS', '\\Deleted')
# supprimer tous les messages selectionnés
conn.expunge()
print("Script terminé")
else:
print("Script annulé")
# deconnexion du serveur
conn.close()
conn.logout()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env python3
import imaplib
import msal
import pprint
conf = {
"authority": "https://login.microsoftonline.com/47f02128-acf0-47a7-a3bf-27a3619d4a4f",
"client_id": "4fdf2634-a260-4576-a442-684998f0187b", #AppID
"scope": ['https://outlook.office365.com/.default'],
"secret": "dZD8Q~GmG_PFi.t_uRiyBEzwLeMZeemQGHUFta~J", #Key-Value
}
def generate_auth_string(user, token):
return f"user={user}\x01auth=Bearer {token}\x01\x01"
if __name__ == "__main__":
app = msal.ConfidentialClientApplication(conf['client_id'], authority=conf['authority'],
client_credential=conf['secret'])
result = app.acquire_token_silent(conf['scope'], account=None)
if not result:
print("No suitable token in cache. Get new one.")
result = app.acquire_token_for_client(scopes=conf['scope'])
if "access_token" in result:
print(result['token_type'])
pprint.pprint(result)
else:
print(result.get("error"))
print(result.get("error_description"))
print(result.get("correlation_id"))
imap = imaplib.IMAP4('outlook.office365.com')
imap.starttls()
imap.authenticate("XOAUTH2", lambda x: generate_auth_string("peinture-dumas@entreprise-dumas.com", result['access_token']).encode("utf-8"))
# Print list of mailboxes on server
code, mailboxes = imap.list()
for mailbox in mailboxes:
print(mailbox.decode("utf-8"))
# Select mailbox
imap.select("INBOX")
# Cleanup
imap.close()

20
imap-SSL-alinto-dumas.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
import imaplib
if __name__ == "__main__":
# connecter au serveur IMAP avec SSL
# authentification basique (email / password)
imap = imaplib.IMAP4_SSL('imap.entreprise-dumas.com')
# se connecter à la mailbox
mbx_name = 'peinture-dumas@entreprise-dumas.com'
mbx_pwd = 'S@sdumas69'
imap.login(mbx_name, mbx_pwd)
# Print list of mailboxes on server
code, mailboxes = imap.list()
for mailbox in mailboxes:
print(mailbox.decode("unicode"))
# Select mailbox
imap.select("INBOX")
# Cleanup
imap.close()

View File

@@ -4,7 +4,7 @@
###
[app:main]
use = egg:cao_blogr
use = egg:ctp_blogr
pyramid.reload_templates = false
pyramid.debug_authorization = false
@@ -12,12 +12,12 @@ pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en
sqlalchemy.url = sqlite:///%(here)s/cao_blogr.sqlite
sqlalchemy.url = sqlite:///%(here)s/ctp_blogr.sqlite
retry.attempts = 3
[pshell]
setup = cao_blogr.pshell.setup
setup = ctp_blogr.pshell.setup
###
# wsgi server configuration
@@ -25,13 +25,14 @@ setup = cao_blogr.pshell.setup
[alembic]
# path to migration scripts
script_location = cao_blogr/alembic
script_location = ctp_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
listen = *:9280
url_scheme = https
###
# logging configuration
@@ -39,7 +40,7 @@ listen = *:6543
###
[loggers]
keys = root, cao_blogr, sqlalchemy
keys = root, ctp_blogr, sqlalchemy
[handlers]
keys = console
@@ -51,10 +52,10 @@ keys = generic
level = WARN
handlers = console
[logger_cao_blogr]
[logger_ctp_blogr]
level = WARN
handlers =
qualname = cao_blogr
qualname = ctp_blogr
[logger_sqlalchemy]
level = WARN

View File

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

View File

@@ -18,13 +18,16 @@ requires = [
'pyramid_retry',
'pyramid_layout',
'pyramid_tm',
'SQLAlchemy',
'SQLAlchemy==1.4.54',
'transaction',
'zope.sqlalchemy',
'zope.sqlalchemy==2.0',
'wtforms', # form library 2.2.1
'webhelpers2', # various web building related helpers 2.0
'passlib',
'python-magic',
'markdown',
'urllib3==1.26',
'yfinance',
]
tests_require = [
@@ -34,9 +37,9 @@ tests_require = [
]
setup(
name='cao_blogr',
name='ctp_blogr',
version='1.0',
description='cao_blogr',
description='ctp_blogr',
long_description=README + '\n\n' + CHANGES,
classifiers=[
'Programming Language :: Python',
@@ -57,10 +60,10 @@ setup(
install_requires=requires,
entry_points={
'paste.app_factory': [
'main = cao_blogr:main',
'main = ctp_blogr:main',
],
'console_scripts': [
'initialize_cao_blogr_db=cao_blogr.scripts.initialize_db:main',
'initialize_ctp_blogr_db=ctp_blogr.scripts.initialize_db:main',
],
},
)

33
smtp-SSL-alinto-dumas.py Normal file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import smtplib
import ssl
# Fabrication du corps du email_passwordMessage
sender = "sasdumas@entreprise-dumas.com"
receiver = "phuoc@caotek.fr"
message = """\
Subject: Envoi d'un message en SSL
Voici le contenu du message. Phuoc"""
# Create a secure SSL context
context = ssl.create_default_context()
smtp_server = "gatewayxl.alinto.net"
smtp_port = 465 # For SSL
smtp_user = "smtpsasdumas@entreprise-dumas.com"
smtp_pass = "S@sdumas69"
# Try to log in to server and send email
try:
server = smtplib.SMTP_SSL(smtp_server, smtp_port, context=context)
server.login(smtp_user, smtp_pass)
# envoyer l'email
server.sendmail(sender, receiver, message)
print("sendmail -> OK")
except Exception as e:
# Print any error messages to stdout
print(e)
finally:
server.quit()

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import smtplib
import ssl
# Fabrication du corps du email_passwordMessage
sender = "peinture-dumas@entreprise-dumas.com"
receiver = "phuoc@caotek.fr"
message = """\
Subject: Envoi d'un message en TLS
Voici le contenu du message. Phuoc"""
# Create a secure SSL context
context = ssl.create_default_context()
smtp_server = "smtp.office365.com"
smtp_port = 587 # For TLS
smtp_user = "peinture-dumas@entreprise-dumas.com"
smtp_pass = "Nar50611"
# Try to log in to server and send email
try:
server = smtplib.SMTP(smtp_server,smtp_port)
# server.ehlo() # Can be omitted
server.starttls(context=context) # Secure the connection
# server.ehlo() # Can be omitted
server.login(smtp_user, smtp_pass)
# envoyer l'email
server.sendmail(sender, receiver, message)
print("sendmail -> OK")
except Exception as e:
# Print any error messages to stdout
print(e)
finally:
server.quit()