diff --git a/modules/agendadulibre/__init__.py b/modules/agendadulibre/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..354c3fbec682215ff0f32212c83c7688bf3b0671 --- /dev/null +++ b/modules/agendadulibre/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2014 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from .backend import AgendadulibreBackend + + +__all__ = ['AgendadulibreBackend'] diff --git a/modules/agendadulibre/backend.py b/modules/agendadulibre/backend.py new file mode 100644 index 0000000000000000000000000000000000000000..040e543d9f903d72450fd10402c51a12a95c3220 --- /dev/null +++ b/modules/agendadulibre/backend.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2014 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.backend import BaseBackend, BackendConfig +from weboob.capabilities.calendar import CapCalendarEvent, CATEGORIES +from weboob.tools.ordereddict import OrderedDict +from weboob.tools.value import Value + +from .browser import AgendadulibreBrowser + + +__all__ = ['AgendadulibreBackend'] + + +class AgendadulibreBackend(BaseBackend, CapCalendarEvent): + NAME = 'agendadulibre' + DESCRIPTION = u'agendadulibre website' + MAINTAINER = u'Bezleputh' + EMAIL = 'carton_ben@yahoo.fr' + LICENSE = 'AGPLv3+' + VERSION = '1.0' + ASSOCIATED_CATEGORIES = [CATEGORIES.CONF] + BROWSER = AgendadulibreBrowser + + region_choices = OrderedDict([(k, u'%s (%s)' % (v, k)) for k, v in sorted({ + "http://www.agendadulibre.org": u'--France--', + "http://www.agendadulibre.org#1": u'Alsace', + "http://www.agendadulibre.org#2": u'Aquitaine', + "http://www.agendadulibre.org#3": u'Auvergne', + "http://www.agendadulibre.org#4": u'Basse-Normandie', + "http://www.agendadulibre.org#5": u'Bourgogne', + "http://www.agendadulibre.org#6": u'Bretagne', + "http://www.agendadulibre.org#7": u'Centre', + "http://www.agendadulibre.org#8": u'Champagne-Ardenne', + "http://www.agendadulibre.org#9": u'Corse', + "http://www.agendadulibre.org#10": u'Franche-Comté', + "http://www.agendadulibre.org#23": u'Guadeloupe', + "http://www.agendadulibre.org#24": u'Guyane', + "http://www.agendadulibre.org#11": u'Haute-Normandie', + "http://www.agendadulibre.org#12": u'Île-de-France', + "http://www.agendadulibre.org#13": u'Languedoc-Roussillon', + "http://www.agendadulibre.org#14": u'Limousin', + "http://www.agendadulibre.org#15": u'Lorraine', + "http://www.agendadulibre.org#25": u'Martinique', + "http://www.agendadulibre.org#16": u'Midi-Pyrénées', + "http://www.agendadulibre.org#17": u'Nord-Pas-de-Calais', + "http://www.agendadulibre.org#18": u'Pays de la Loire', + "http://www.agendadulibre.org#19": u'Picardie', + "http://www.agendadulibre.org#20": u'Poitou-Charentes', + "http://www.agendadulibre.org#21": u'Provence-Alpes-Côte d\'Azur', + "http://www.agendadulibre.org#26": u'Réunion', + "http://www.agendadulibre.org#22": u'Rhône-Alpes', + "http://www.agendadulibre.be": u'--Belgique--', + "http://www.agendadulibre.be#11": u'Antwerpen', + "http://www.agendadulibre.be#10": u'Brabant wallon', + "http://www.agendadulibre.be#9": u'Bruxelles-Capitale', + "http://www.agendadulibre.be#8": u'Hainaut', + "http://www.agendadulibre.be#7": u'Liege', + "http://www.agendadulibre.be#6": u'Limburg', + "http://www.agendadulibre.be#5": u'Luxembourg', + "http://www.agendadulibre.be#4": u'Namur', + "http://www.agendadulibre.be#3": u'Oost-Vlaanderen', + "http://www.agendadulibre.be#2": u'Vlaams-Brabant', + "http://www.agendadulibre.be#1": u'West-Vlaanderen', + "http://www.agendadulibre.ch": u'--Suisse--', + "http://www.agendadulibre.ch#15": u'Appenzell Rhodes-Extérieures', + "http://www.agendadulibre.ch#16": u'Appenzell Rhodes-Intérieures', + "http://www.agendadulibre.ch#19": u'Argovie', + "http://www.agendadulibre.ch#13": u'Bâle-Campagne', + "http://www.agendadulibre.ch#12": u'Bâle-Ville', + "http://www.agendadulibre.ch#2": u'Berne', + "http://www.agendadulibre.ch#10": u'Fribourg', + "http://www.agendadulibre.ch#25": u'Genève', + "http://www.agendadulibre.ch#8": u'Glaris', + "http://www.agendadulibre.ch#18": u'Grisons', + "http://www.agendadulibre.ch#26": u'Jura', + "http://www.agendadulibre.ch#3": u'Lucerne', + "http://www.agendadulibre.ch#24": u'Neuchâtel', + "http://www.agendadulibre.ch#7": u'Nidwald', + "http://www.agendadulibre.ch#6": u'Obwald', + "http://www.agendadulibre.ch#17": u'Saint-Gall', + "http://www.agendadulibre.ch#14": u'Schaffhouse', + "http://www.agendadulibre.ch#5": u'Schwytz', + "http://www.agendadulibre.ch#11": u'Soleure', + "http://www.agendadulibre.ch#21": u'Tessin', + "http://www.agendadulibre.ch#20": u'Thurgovie', + "http://www.agendadulibre.ch#4": u'Uri', + "http://www.agendadulibre.ch#23": u'Valais', + "http://www.agendadulibre.ch#22": u'Vaud', + "http://www.agendadulibre.ch#9": u'Zoug', + "http://www.agendadulibre.ch#1": u'Zurich', + }.iteritems())]) + + CONFIG = BackendConfig(Value('region', label=u'Region', choices=region_choices)) + + def create_default_browser(self): + choice = self.config['region'].get().split('#') + selected_region = '' if len(choice) < 2 else choice[-1] + return self.create_browser(website=choice[0], region=selected_region) + + def search_events(self, query): + return self.browser.list_events(query.start_date, + query.end_date, + query.city, + query.categories) + + def list_events(self, date_from, date_to=None): + return self.browser.list_events(date_from, date_to) + + def get_event(self, event_id): + return self.browser.get_event(event_id) + + def fill_obj(self, event, fields): + return self.browser.get_event(event.id, event) + + OBJECTS = {AgendadulibreBrowser: fill_obj} diff --git a/modules/agendadulibre/browser.py b/modules/agendadulibre/browser.py new file mode 100644 index 0000000000000000000000000000000000000000..42cb94340cbd083eab3fd3770c228478ceecb23c --- /dev/null +++ b/modules/agendadulibre/browser.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2014 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.browser2 import PagesBrowser, URL + +from .pages import EventListPage, EventPage +from datetime import timedelta, date + + +class AgendadulibreBrowser(PagesBrowser): + + event_list_page = URL('events\?start_date=(?P.*)(?P.*)', EventListPage) + event_page = URL('events/(?P<_id>.*)', EventPage) + + def __init__(self, website, region, *args, **kwargs): + self.BASEURL = u'%s' % website + self.region = '®ion=%s' % region if region else '' + PagesBrowser.__init__(self, *args, **kwargs) + + def list_events(self, date_from, date_to, city=None, categories=None, max_date=None): + _max_date = date_from + timedelta(days=365) + max_date = date(year=_max_date.year, month=_max_date.month, day=_max_date.day) + return self.event_list_page.go(date_from=date_from.strftime("%Y-%m-%d"), + region=self.region)\ + .list_events(date_from=date_from, + date_to=date_to, + city=city, + categories=categories, + max_date=max_date) + + def get_event(self, event_id, event=None): + _id = event_id.split('#')[-1] + return self.event_page.go(_id=_id).get_event(obj=event) diff --git a/modules/agendadulibre/calendar.py b/modules/agendadulibre/calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..a80dfb69842fe076d638650fc655b3a0221fb5b6 --- /dev/null +++ b/modules/agendadulibre/calendar.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.capabilities.calendar import BaseCalendarEvent, TRANSP, STATUS, CATEGORIES + + +class AgendaDuLibreCalendarEvent(BaseCalendarEvent): + + def __init__(self): + BaseCalendarEvent.__init__(self) + self.sequence = 1 + self.transp = TRANSP.TRANSPARENT + self.status = STATUS.CONFIRMED + self.category = CATEGORIES.CONF diff --git a/modules/agendadulibre/pages.py b/modules/agendadulibre/pages.py new file mode 100644 index 0000000000000000000000000000000000000000..aed928b773c89e9ecdddad34a5660ffa33e17a9a --- /dev/null +++ b/modules/agendadulibre/pages.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2014 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + +from weboob.tools.browser2.page import HTMLPage, method, pagination +from weboob.tools.browser2.elements import ItemElement, ListElement +from weboob.tools.browser2.filters import Regexp, Link, CleanText, DateTime, Filter, Type, Env, XPath, Format, CleanHTML, CombineDate + +from .calendar import AgendaDuLibreCalendarEvent +from datetime import time, datetime, date +import re + + +class EventEndDate(Filter): + def filter(self, el): + return time.max + + +class EventPage(HTMLPage): + @method + class get_event(ItemElement): + klass = AgendaDuLibreCalendarEvent + + def parse(self, el): + self.env['url'] = self.page.url + + obj_id = Env('_id') + obj_url = Env('url') + obj_summary = CleanText('//meta[@property="DC:title"]/@content') + obj_description = CleanHTML('//div[@class="description"]') + obj_location = CleanText('//p[@class="full_address"]/span[1]') + obj_city = CleanText('//meta[@property="geo:placename"]/@content') + obj_start_date = DateTime(CleanText('//meta[@property="DC:date"]/@content')) + obj_end_date = CombineDate(DateTime(CleanText('//meta[@property="DC:date"]/@content')), + EventEndDate('.')) + + +class EventListPage(HTMLPage): + @pagination + @method + class list_events(ListElement): + item_xpath = '//td[starts-with(@class, "day")]/ul/li' + + def next_page(self): + m = re.match('.*/events\?start_date=(\d{4})-(\d{2})-(\d{2})(®ion=.*)?', self.page.url) + if m: + start = date(year=int(m.group(1)), month=int(m.group(2)), day=int(m.group(3))) + region = m.group(4) if m.group(4) else '' + try: + next_month = start.replace(month=start.month + 1) + except ValueError: + if start.month == 12: + next_month = start.replace(year=start.year + 1, month=1) + else: + raise + if (self.env['date_to'] is None and + start < self.env['max_date']) or\ + (self.env['date_to'] is not None and + datetime.combine(next_month, time.min) < self.env['date_to']): + return '/events?start_date=%s%s' % (next_month.strftime("%Y-%m-%d"), region) + + class item(ItemElement): + klass = AgendaDuLibreCalendarEvent + + def condition(self): + return len(XPath('.')(self.el)) > 0 and \ + ('current-month' in XPath('./ancestor::td/@class')(self.el)[0]) + obj_id = Format('%s#%s', + CleanText('./ancestor::td/div[@class="day_number"]'), + Regexp(Link('./a'), '/events/(.*)')) + obj_city = CleanText('./a/strong[@class="city"]') + obj_summary = CleanText('./a') + + def get_date(self, _time): + m = re.match('.*/events\?start_date=(\d{4})-(\d{2})-\d{2}', self.page.url) + if m: + day = Type(CleanText('./ancestor::td/div[@class="day_number"]'), type=int)(self) + start_date = date(year=int(m.group(1)), month=int(m.group(2)), day=day) + return datetime.combine(start_date, _time) + + def obj_start_date(self): + return self.get_date(time.min) + + def obj_end_date(self): + return self.get_date(time.max) + + def validate(self, obj): + return (self.is_valid_event(obj, self.env['city'], self.env['categories']) and + self.is_event_in_valid_period(obj.start_date, self.env['date_from'], self.env['date_to'])) + + def is_valid_event(self, event, city, categories): + if city and city != '' and city.upper() != event.city.upper(): + return False + if categories and len(categories) > 0 and event.category not in categories: + return False + return True + + def is_event_in_valid_period(self, event_date, date_from, date_to): + if event_date >= datetime.combine(date_from, time.min): + if not date_to: + return True + else: + if event_date <= date_to: + return True + return False diff --git a/modules/agendadulibre/test.py b/modules/agendadulibre/test.py new file mode 100644 index 0000000000000000000000000000000000000000..ded48c43218e3e2c519bec5f099be2231b35d1e8 --- /dev/null +++ b/modules/agendadulibre/test.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2014 Bezleputh +# +# This file is part of weboob. +# +# weboob is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# weboob is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with weboob. If not, see . + + +from weboob.tools.test import BackendTest +from datetime import datetime + + +class AgendadulibreTest(BackendTest): + BACKEND = 'agendadulibre' + + def test_agendadulibre(self): + l = list(self.backend.list_events(datetime.now())) + assert len(l) + event = self.backend.get_event(l[0].id) + self.assertTrue(event.url, 'URL for event "%s" not found: %s' % (event.id, event.url))