diff options
Diffstat (limited to 'pyaggr3g470r')
-rw-r--r-- | pyaggr3g470r/__init__.py | 11 | ||||
-rw-r--r-- | pyaggr3g470r/feedgetter.py | 44 | ||||
-rw-r--r-- | pyaggr3g470r/forms.py | 11 | ||||
-rw-r--r-- | pyaggr3g470r/models.py | 129 | ||||
-rw-r--r-- | pyaggr3g470r/templates/feed.html | 12 | ||||
-rw-r--r-- | pyaggr3g470r/templates/home.html | 16 | ||||
-rw-r--r-- | pyaggr3g470r/views.py | 170 |
7 files changed, 225 insertions, 168 deletions
diff --git a/pyaggr3g470r/__init__.py b/pyaggr3g470r/__init__.py index 16bdc57f..d29b1373 100644 --- a/pyaggr3g470r/__init__.py +++ b/pyaggr3g470r/__init__.py @@ -2,12 +2,10 @@ # -*- coding: utf-8 -*- import os - from flask import Flask -from flask.ext.mongoengine import MongoEngine +from flask.ext.sqlalchemy import SQLAlchemy import conf -from models import * # Create Flask application app = Flask(__name__) @@ -15,8 +13,9 @@ app.debug = True # Create dummy secrey key so we can use sessions app.config['SECRET_KEY'] = os.urandom(12) +app.config['SQLALCHEMY_DATABASE_URI'] = conf.SQLALCHEMY_DATABASE_URI +db = SQLAlchemy(app) -app.config['MONGODB_SETTINGS'] = {'DB': conf.DATABASE_NAME} app.config["MAIL_SERVER"] = conf.MAIL_HOST app.config["MAIL_PORT"] = conf.MAIL_PORT app.config["MAIL_USE_TLS"] = conf.MAIL_TLS @@ -24,10 +23,6 @@ app.config["MAIL_USE_SSL"] = conf.MAIL_SSL app.config["MAIL_USERNAME"] = conf.MAIL_USERNAME app.config["MAIL_PASSWORD"] = conf.MAIL_PASSWORD -# Initializes the database -db = MongoEngine(app) -db.init_app(app) - from flask.ext.mail import Message, Mail mail = Mail(app) diff --git a/pyaggr3g470r/feedgetter.py b/pyaggr3g470r/feedgetter.py index 2325a26e..1c834b88 100644 --- a/pyaggr3g470r/feedgetter.py +++ b/pyaggr3g470r/feedgetter.py @@ -34,9 +34,10 @@ from datetime import datetime from urllib import urlencode from urlparse import urlparse, parse_qs, urlunparse from BeautifulSoup import BeautifulSoup -from mongoengine.queryset import NotUniqueError + from requests.exceptions import Timeout +from sqlalchemy.exc import IntegrityError import models import conf @@ -44,7 +45,8 @@ import search import utils from flask.ext.mail import Message -from pyaggr3g470r import app, mail +from pyaggr3g470r import app, db, mail +from pyaggr3g470r.models import User, Feed, Article import log pyaggr3g470r_log = log.Log("feedgetter") @@ -73,7 +75,7 @@ class FeedGetter(object): "https": "http://" + conf.HTTP_PROXY } feedparser.USER_AGENT = conf.USER_AGENT - self.user = models.User.objects(email=email).first() + self.user = User.query.filter(User.email == email).first() def retrieve_feed(self, feed_id=None): """ @@ -81,7 +83,7 @@ class FeedGetter(object): """ feeds = [feed for feed in self.user.feeds if feed.enabled] if feed_id != None: - feeds = [feed for feed in feeds if str(feed.oid) == feed_id] + feeds = [feed for feed in feeds if str(feed.id) == feed_id] for current_feed in feeds: try: # launch a new thread for the RSS feed @@ -96,8 +98,6 @@ class FeedGetter(object): for th in list_of_threads: th.join() - self.user.save() - def process(self, feed): """ Retrieves articles form the feed and add them to the database. @@ -170,24 +170,17 @@ class FeedGetter(object): post_date = datetime(*article.updated_parsed[:6]) # save the article - article = models.Article(post_date, nice_url, article_title, description, False, False) - try: - article.save() - articles.append(article) - pyaggr3g470r_log.info("New article %s (%s) added." % (article_title, nice_url)) - except NotUniqueError: - pyaggr3g470r_log.error("Article %s (%s) already in the database." % (article_title, nice_url)) - continue - except Exception as e: - pyaggr3g470r_log.error("Error when inserting article in database: " + str(e)) - continue + article = Article(link=nice_url, title=article_title, + content=description, readed=False, like=False, date=post_date) + articles.append(article) # add the article to the Whoosh index + """ try: search.add_to_index([article], feed) except Exception as e: pyaggr3g470r_log.error("Whoosh error.") - pass + pass""" # email notification if conf.MAIL_ENABLED and feed.email_notification: @@ -199,8 +192,19 @@ class FeedGetter(object): mail.send(msg) # add the articles to the list of articles for the current feed - feed.articles.extend(articles) - feed.articles = sorted(feed.articles, key=lambda t: t.date, reverse=True) + for article in articles: + try: + feed.articles.append(article) + db.session.merge(article) + db.session.commit() + pyaggr3g470r_log.info("New article %s (%s) added." % (article_title, nice_url)) + except IntegrityError: + pyaggr3g470r_log.error("Article %s (%s) already in the database." % (article_title, nice_url)) + db.session.rollback() + continue + except Exception as e: + pyaggr3g470r_log.error("Error when inserting article in database: " + str(e)) + continue return True diff --git a/pyaggr3g470r/forms.py b/pyaggr3g470r/forms.py index f9bb5a7a..7439c34b 100644 --- a/pyaggr3g470r/forms.py +++ b/pyaggr3g470r/forms.py @@ -27,11 +27,15 @@ __copyright__ = "Copyright (c) Cedric Bonhomme" __license__ = "GPLv3" from flask.ext.wtf import Form +from flask import flash from wtforms import TextField, TextAreaField, PasswordField, BooleanField, SubmitField, validators -import models +from pyaggr3g470r.models import User class SigninForm(Form): + """ + Sign in form. + """ email = TextField("Email", [validators.Required("Please enter your email address."), validators.Email("Please enter your email address.")]) password = PasswordField('Password', [validators.Required("Please enter a password.")]) submit = SubmitField("Log In") @@ -43,11 +47,12 @@ class SigninForm(Form): if not Form.validate(self): return False - user = models.User.objects(email = self.email.data).first() + user = User.query.filter(User.email == self.email.data).first() if user and user.check_password(self.password.data): return True else: - self.email.errors.append("Invalid e-mail or password") + flash('Invalid email or password', 'danger') + #self.email.errors.append("Invalid email or password") return False class AddFeedForm(Form): diff --git a/pyaggr3g470r/models.py b/pyaggr3g470r/models.py index cbd49350..0fd9eb73 100644 --- a/pyaggr3g470r/models.py +++ b/pyaggr3g470r/models.py @@ -26,86 +26,97 @@ __revision__ = "$Date: 2013/11/16 $" __copyright__ = "Copyright (c) Cedric Bonhomme" __license__ = "GPLv3" -from mongoengine import * from datetime import datetime - from werkzeug import generate_password_hash, check_password_hash from flask.ext.login import UserMixin +from pyaggr3g470r import db -import bson.objectid - -class User(Document, UserMixin): +class User(db.Model, UserMixin): """ - Defines the model for a user. + Represent a user. """ - firstname = StringField(required=True) - lastname = StringField(required = True) - email = EmailField(required=True, unique=True) - pwdhash = StringField(required=True) - feeds = ListField(EmbeddedDocumentField('Feed')) - created_at = DateTimeField(required=True, default=datetime.now) + id = db.Column(db.Integer, primary_key = True) + firstname = db.Column(db.String()) + lastname = db.Column(db.String()) + email = db.Column(db.String(), index = True, unique = True) + pwdhash = db.Column(db.String()) + roles = db.relationship('Role', backref = 'user', lazy = 'dynamic') + date_created = db.Column(db.DateTime(), default=datetime.now) + last_seen = db.Column(db.DateTime(), default=datetime.now) + feeds = db.relationship('Feed', backref = 'subscriber', lazy = 'dynamic', cascade='all,delete-orphan') def get_id(self): + """ + Return the id (email) of the user. + """ return self.email def set_password(self, password): + """ + Hash the password of the user. + """ self.pwdhash = generate_password_hash(password) def check_password(self, password): + """ + Check the password of the user. + """ return check_password_hash(self.pwdhash, password) - #required for administrative interface - def __unicode__(self): - return self.email - -class Feed(EmbeddedDocument): - """ - Defines the model for a feed. - """ - oid = ObjectIdField(default=bson.objectid.ObjectId , primary_key=True) - title = StringField(required=True) - description = StringField(default="") - link = StringField(required=True, unique=True) - site_link = StringField() - email_notification = BooleanField(default=False) - enabled = BooleanField(default=True) - articles = ListField(ReferenceField('Article', dbref = False)) - created_date = DateTimeField(required=True, default=datetime.now) - - meta = { - 'ordering': ['+title'] - } + def is_admin(self): + """ + Return True if the user has administrator rights. + """ + return len([role for role in self.roles if role.name == "admin"]) != 0 def __eq__(self, other): - return self.oid == other.oid + return self.id == other.id - def __str__(self): - return 'Feed: %s' % self.title + def __repr__(self): + return '<User %r>' % (self.firstname) -class Article(Document): +class Role(db.Model): """ - Defines the model for an article. + Represent a role. """ - date = DateTimeField(required=True) - link = StringField(required=True, unique=True) - title = StringField(required=True) - content = StringField(required=True) - readed = BooleanField() - like = BooleanField() - retrieved_date = DateTimeField(required=True, default=datetime.now) - - meta = { - 'ordering': ['-date'], - 'indexes': [ - {'fields': ['-date'], - 'sparse': True, 'types': False }, - {'fields': ['link'], - 'sparse': True, 'unique': True, 'types': False } - ] - } + id = db.Column(db.Integer, primary_key = True) + name = db.Column(db.String(), unique = True) - def __eq__(self, other): - return self.link == other + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) - def __str__(self): - return 'Article: %s' % self.title +class Feed(db.Model): + """ + Represent a station. + """ + id = db.Column(db.Integer, primary_key = True) + title = db.Column(db.String(), default="New station") + description = db.Column(db.String(), default="FR") + link = db.Column(db.String()) + site_link = db.Column(db.String(), default="New station") + email_notification = db.Column(db.Boolean(), default=False) + enabled = db.Column(db.Boolean(), default=True) + created_date = db.Column(db.DateTime(), default=datetime.now) + articles = db.relationship('Article', backref = 'feed', lazy = 'dynamic', cascade='all,delete-orphan') + + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + + def __repr__(self): + return '<Feed %r>' % (self.title) + +class Article(db.Model): + """ + Represent an article from a feed. + """ + id = db.Column(db.Integer, primary_key = True) + link = db.Column(db.String(), unique = True) + title = db.Column(db.String()) + content = db.Column(db.String()) + readed = db.Column(db.Boolean(), default=False) + like = db.Column(db.Boolean(), default=False) + date = db.Column(db.DateTime(), default=datetime.now) + retrieved_date = db.Column(db.DateTime(), default=datetime.now) + + station_id = db.Column(db.Integer, db.ForeignKey('feed.id')) + + def __repr__(self): + return '<Article %r>' % (self.title) diff --git a/pyaggr3g470r/templates/feed.html b/pyaggr3g470r/templates/feed.html index 95465170..849dba49 100644 --- a/pyaggr3g470r/templates/feed.html +++ b/pyaggr3g470r/templates/feed.html @@ -4,14 +4,14 @@ <div class="jumbotron"> <h2>{{ feed.title }}</h2> {% if feed.description %} <p>{{ feed.description }}</p> {% endif %} - <a href="/delete_feed/{{ feed.oid }}"><i class="glyphicon glyphicon-remove" title="Delete this feed"></i></a> - <a href="/edit_feed/{{ feed.oid }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> + <a href="/delete_feed/{{ feed.id }}"><i class="glyphicon glyphicon-remove" title="Delete this feed"></i></a> + <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> </div> <div class="jumbotron"> <p> - This feed contains {{ feed.articles|count }} <a href="/articles/{{ feed.oid }}/100">articles</a> + This feed contains {{ feed.articles.all()|count }} <a href="/articles/{{ feed.id }}/100">articles</a> {% if nb_articles != 0 %} - ({{ ((feed.articles|count * 100 ) / nb_articles) | round(2, 'floor') }}% of the database) + ({{ ((feed.articles.all()|count * 100 ) / nb_articles) | round(2, 'floor') }}% of the database) {% endif %} .<br /> Address of the feed: <a href="{{ feed.link }}">{{ feed.link }}</a><br /> @@ -19,14 +19,14 @@ Address of the site: <a href="{{ feed.site_link }}">{{ feed.site_link }}</a> {% endif %} <br /> - {% if feed.articles|count != 0 %} + {% if feed.articles.all()|count != 0 %} The last article was posted {{ elapsed.days }} day(s) ago.<br /> Daily average: {{ average }}, between the {{ first_post_date.strftime('%Y-%m-%d') }} and the {{ end_post_date.strftime('%Y-%m-%d') }}. {% endif %} </p> </div> <div class="jumbotron"> - {% if feed.articles|count != 0 %} + {% if feed.articles.all()|count != 0 %} <div>{{ tag_cloud|safe }}</div> {% endif %} </div> diff --git a/pyaggr3g470r/templates/home.html b/pyaggr3g470r/templates/home.html index 9117bce9..8a4e429d 100644 --- a/pyaggr3g470r/templates/home.html +++ b/pyaggr3g470r/templates/home.html @@ -8,16 +8,16 @@ <div class="row"> <div class="col-md-6 col-md-offset-3"> <h1>{{ feed.title|safe }}</h1> - <a href="/articles/{{ feed.oid }}/100"><i class="glyphicon glyphicon-th-list" title="More articles"></i></a> - <a href="/feed/{{ feed.oid }}"><i class="glyphicon glyphicon-info-sign" title="Details"></i></a> - <a href="/edit_feed/{{ feed.oid }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> + <a href="/articles/{{ feed.id }}/100"><i class="glyphicon glyphicon-th-list" title="More articles"></i></a> + <a href="/feed/{{ feed.id }}"><i class="glyphicon glyphicon-info-sign" title="Details"></i></a> + <a href="/edit_feed/{{ feed.id }}"><i class="glyphicon glyphicon-edit" title="Edit this feed"></i></a> {% if feed.enabled %} - <a href="/fetch/{{ feed.oid }}"><i class="glyphicon glyphicon-cloud-download" title="Fetch this feed"></i></a> + <a href="/fetch/{{ feed.id }}"><i class="glyphicon glyphicon-cloud-download" title="Fetch this feed"></i></a> {% endif %} - <a href="/mark_as_read/{{ feed.oid }}"><i class="glyphicon glyphicon-check" title="Mark all as read"></i></a> + <a href="/mark_as_read/{{ feed.id }}"><i class="glyphicon glyphicon-check" title="Mark all as read"></i></a> </div> </div> - {% for number in range(0, feed.articles|length-(feed.articles|length % 3), 3) %} + {% for number in range(0, feed.articles.all()|count-(feed.articles.all()|count % 3), 3) %} <div class="row"> {% for n in range(number, number+3) %} <div class="col-xs-6 col-sm-4 col-md-4"> @@ -29,9 +29,9 @@ {% endfor %} </div> {% endfor %} - {% if feed.articles|length % 3 != 0 %} + {% if feed.articles.all()|count % 3 != 0 %} <div class="row"> - {% for n in range(feed.articles|length-(feed.articles|length % 3), feed.articles|length) %} + {% for n in range(feed.articles.all()|count-(feed.articles.all()|count % 3), feed.articles.all()|count) %} <div class="col-xs-6 col-sm-4 col-md-4"> {% if feed.articles[n].readed %}<h3>{% else %}<h1>{% endif %} <a href="/article/{{ feed.articles[n].id }}">{{ feed.articles[n].title|safe }}</a> diff --git a/pyaggr3g470r/views.py b/pyaggr3g470r/views.py index 8dc4c7c7..b2811b66 100644 --- a/pyaggr3g470r/views.py +++ b/pyaggr3g470r/views.py @@ -27,9 +27,10 @@ __copyright__ = "Copyright (c) Cedric Bonhomme" __license__ = "GPLv3" import os -import datetime -from flask import render_template, request, make_response, flash, session, url_for, redirect, g +from datetime import datetime +from flask import render_template, jsonify, request, flash, session, url_for, redirect, g, current_app, make_response from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user, AnonymousUserMixin +from flask.ext.principal import Principal, Identity, AnonymousIdentity, identity_changed, identity_loaded, Permission, RoleNeed, UserNeed import conf import utils @@ -39,10 +40,29 @@ import models import search as fastsearch from forms import SigninForm, AddFeedForm, ProfileForm from pyaggr3g470r import app, db +from pyaggr3g470r.models import User, Feed, Article, Role login_manager = LoginManager() login_manager.init_app(app) +# +# Management of the user's session. +# +@identity_loaded.connect_via(app) +def on_identity_loaded(sender, identity): + # Set the identity user object + identity.user = current_user + + # Add the UserNeed to the identity + if hasattr(current_user, 'id'): + identity.provides.add(UserNeed(current_user.id)) + + # Assuming the User model has a list of roles, update the + # identity with the roles that the user provides + if hasattr(current_user, 'roles'): + for role in current_user.roles: + identity.provides.add(RoleNeed(role.name)) + @app.before_request def before_request(): g.user = current_user @@ -65,17 +85,32 @@ def authentication_failed(e): @login_manager.user_loader def load_user(email): # Return an instance of the User model - return models.User.objects(email=email).first() + return User.query.filter(User.email == email).first() + +def redirect_url(default='index'): + return request.args.get('next') or \ + request.referrer or \ + url_for(default) + + + +# +# Views. +# @app.route('/login/', methods=['GET', 'POST']) def login(): + """ + Log in view. + """ g.user = AnonymousUserMixin() form = SigninForm() if form.validate_on_submit(): - user = models.User.objects(email=form.email.data).first() + user = User.query.filter(User.email == form.email.data).first() login_user(user) g.user = user + identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) flash("Logged in successfully.", 'success') return redirect(url_for('home')) return render_template('login.html', form=form) @@ -84,19 +119,25 @@ def login(): @login_required def logout(): """ - Remove the user information from the session. + Log out view. Removes the user information from the session. """ - logout_user() - flash("Logged out successfully.", 'success') - return redirect(url_for('home')) + # Update last_seen field + g.user.last_seen = datetime.utcnow() + db.session.add(g.user) + db.session.commit() -def redirect_url(default='index'): - return request.args.get('next') or \ - request.referrer or \ - url_for(default) + # Remove the user information from the session + logout_user() + # Remove session keys set by Flask-Principal + for key in ('identity.name', 'identity.auth_type'): + session.pop(key, None) + # Tell Flask-Principal the user is anonymous + identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) + flash("Logged out successfully.", 'success') + return redirect(url_for('map_view')) @app.route('/') @login_required @@ -104,10 +145,13 @@ def home(): """ The home page lists most recent articles of all feeds. """ - user = g.user - feeds = models.User.objects(email=g.user.email).fields(slice__feeds__articles=9).first().feeds + user = User.query.filter(User.email == g.user.email).first() + feeds = [] + for feed in user.feeds: + feed.articles = feed.articles[:8] + feeds.append(feed) return render_template('home.html', user=user, feeds=feeds, \ - head_title=models.Article.objects(readed=False).count()) + head_title="nb unread") @app.route('/fetch/', methods=['GET']) @app.route('/fetch/<feed_id>', methods=['GET']) @@ -135,60 +179,58 @@ def feeds(): """ Lists the subscribed feeds in a table. """ - feeds = models.User.objects(email=g.user.email).first().feeds + user = User.query.filter(User.email == g.user.email).first() + feeds = user.feeds return render_template('feeds.html', feeds=feeds) -@app.route('/feed/<feed_id>', methods=['GET']) +@app.route('/feed/<int:feed_id>', methods=['GET']) @login_required def feed(feed_id=None): """ Presents detailed information about a feed. """ - word_size = 6 - nb_articles = models.Article.objects().count() - user = models.User.objects(email=g.user.email, feeds__oid=feed_id).first() - if user == None: - return redirect(url_for('feeds')) - for feed in user.feeds: - if str(feed.oid) == feed_id: - articles = feed.articles - top_words = utils.top_words(articles, n=50, size=int(word_size)) - tag_cloud = utils.tag_cloud(top_words) - - today = datetime.datetime.now() - try: - last_article = articles[0].date - first_article = articles[-1].date - delta = last_article - first_article - average = round(float(len(articles)) / abs(delta.days), 2) - except: - last_article = datetime.datetime.fromtimestamp(0) - first_article = datetime.datetime.fromtimestamp(0) - delta = last_article - first_article - average = 0 - elapsed = today - last_article - - return render_template('feed.html', head_title=utils.clear_string(feed.title), feed=feed, tag_cloud=tag_cloud, \ - first_post_date=first_article, end_post_date=last_article , nb_articles=nb_articles, \ - average=average, delta=delta, elapsed=elapsed) + feed = Feed.query.filter(Feed.id == feed_id).first() + if feed.subscriber.id == g.user.id: + word_size = 6 + articles = feed.articles + nb_articles = len(feed.articles.all()) + top_words = utils.top_words(articles, n=50, size=int(word_size)) + tag_cloud = utils.tag_cloud(top_words) + + today = datetime.now() + try: + last_article = articles[0].date + first_article = articles[-1].date + delta = last_article - first_article + average = round(float(len(articles)) / abs(delta.days), 2) + except: + last_article = datetime.fromtimestamp(0) + first_article = datetime.fromtimestamp(0) + delta = last_article - first_article + average = 0 + elapsed = today - last_article + + return render_template('feed.html', head_title=utils.clear_string(feed.title), feed=feed, tag_cloud=tag_cloud, \ + first_post_date=first_article, end_post_date=last_article , nb_articles=nb_articles, \ + average=average, delta=delta, elapsed=elapsed) else: flash("This feed do not exist.", 'warning') return redirect(redirect_url()) -@app.route('/article/<article_id>', methods=['GET']) +@app.route('/article/<int:article_id>', methods=['GET']) @login_required def article(article_id=None): """ Presents the content of an article. """ - #user = models.User.objects(email=g.user.email, feeds__oid=feed_id).first() - article = models.Article.objects(id=article_id).first() - if article == None: - flash("This article do not exist.", 'warning') - return redirect(redirect_url()) - if not article.readed: - article.readed = True - article.save() + article = Article.query.filter(Article.id == article_id).first() + if article.feed.subscriber.id == g.user.id: + if article == None: + flash("This article do not exist.", 'warning') + return redirect(redirect_url()) + if not article.readed: + article.readed = True + db.session.commit() return render_template('article.html', head_title=utils.clear_string(article.title), article=article) @app.route('/mark_as_read/', methods=['GET']) @@ -494,22 +536,22 @@ def delete_feed(feed_id=None): @login_required def profile(): """ - Edit the profile of the user. + Edit the profile of the currently logged user. """ - user = models.User.objects(email=g.user.email).first() + user = User.query.filter(User.email == g.user.email).first() form = ProfileForm() if request.method == 'POST': - if form.validate() == False: + if form.validate(): + form.populate_obj(user) + if form.password.data != "": + user.set_password(form.password.data) + db.session.commit() + flash('User "' + user.firstname + '" successfully updated.', 'success') + return redirect(url_for('profile')) + else: return render_template('profile.html', form=form) - form.populate_obj(user) - if form.password.data != "": - user.set_password(form.password.data) - user.save() - flash('User "' + user.firstname + '" successfully updated', 'success') - return redirect('/profile/') - if request.method == 'GET': form = ProfileForm(obj=user) - return render_template('profile.html', form=form) + return render_template('profile.html', user=user, form=form) |