# -*- coding: utf-8 -*- """ grumpy.models ~~~~~~~~~~~~~ This module contains the models and low-level database glue for the application. :copyright: (c) by 2010 Priit Laes. :license: BSD, see LICENSE for details. """ from datetime import datetime from flaskext.sqlalchemy import SQLAlchemy from sqlalchemy.sql import func from sqlalchemy.orm.collections import column_mapped_collection import json, random, string, time from . import app from .utils import compare_version db = SQLAlchemy(app) # Association tables package_developers = db.Table('package_developers', db.Model.metadata, db.Column('package_id', db.Integer, db.ForeignKey('packages.id')), db.Column('developer_id', db.Integer, db.ForeignKey('developers.id')) ) package_herds = db.Table('package_herds', db.Model.metadata, db.Column('package_id', db.Integer, db.ForeignKey('packages.id')), db.Column('herd_id', db.Integer, db.ForeignKey('herds.id')) ) package_users = db.Table('package_users', db.Model.metadata, db.Column('package_id', db.Integer, db.ForeignKey('packages.id')), db.Column('user_id', db.Integer, db.ForeignKey('users.id')) ) class Developer(db.Model): """Represents developers in the system""" __tablename__ = 'developers' id = db.Column('id', db.Integer, primary_key=True) email = db.Column('email', db.String, nullable=False, unique=True) def __init__(self, email): self.email = email def __repr__(self): return '<%s> - %s' % (self.__class__.__name__, self.email) class Ebuild(db.Model): """Represents single ebuilds (cpv) in the system""" __tablename__ = 'ebuilds' id = db.Column('id', db.Integer, primary_key=True) cpv = db.Column('cpv', db.String, nullable=False, unique=True) eapi = db.Column('eapi', db.Integer, nullable=False) # Regular USE flags iuse = db.Column('iuse', db.String) # Forced USE flags ('-use' and '+use') iuse_neg = db.Column('iuse_neg', db.String) iuse_pos = db.Column('iuse_pos', db.String) keywords = db.Column('keywords', db.String) slot = db.Column('slot', db.String) revision = db.Column('revision', db.Integer) version = db.Column('version', db.String, nullable=False) package_id = db.Column('package_id', db.Integer, db.ForeignKey('packages.id')) # TODO: depend, rdepend, licenses # TODO: extra info? def __init__(self, ebuild): """Initialize ebuild using pkgcore's ebuild class as an argument.""" self.cpv = ebuild.cpvstr self.version = ebuild.version self.revision = ebuild.revision self.slot = ebuild.slot self.eapi = ebuild.eapi self.keywords = self._parse_keywords(ebuild) self.iuse = self._parse_iuse(ebuild) self.iuse_pos = self._parse_iuse_pos(ebuild) self.iuse_neg = self._parse_iuse_neg(ebuild) def sync(self, ebuild_src): """Update ebuild values from ebuild_src.""" self.__init__(ebuild_src) def rename(self, key): self.cpv = "%s-%s" % (self.package.key, self.version) if self.revision > 0: self.cpv = "%s-r%d" % (self.cpv, self.revision) def _parse_iuse(self, ebuild_src): """Returns list of USE flags from ebuild_src.""" return ','.join([f for f in ebuild_src.iuse if f[0] not in ('+','-')]) def _parse_iuse_neg(self, ebuild_src): """Returns list of forced USE flags (+use) from ebuild_src.""" return ','.join([kw[1:] for kw in ebuild_src.iuse if kw[0] == '-']) def _parse_iuse_pos(self, ebuild_src): """Returns list of forced USE flags (+use) from ebuild_src.""" return ','.join([kw[1:] for kw in ebuild_src.iuse if kw[0] == '+']) def _parse_keywords(self, ebuild_src): """Returns list of keywords from pkgcore's Ebuild.""" return ','.join(ebuild_src.keywords) @property def fullver(self): if self.revision > 0: return "%s-r%d" % (self.version, self.revision) return self.version def __repr__(self): return '<%s> - %s' % (self.__class__.__name__, self.cpv) class Herd(db.Model): """Represents herds in the system""" __tablename__ = 'herds' id = db.Column('id', db.Integer, primary_key=True) name = db.Column('name', db.String, nullable=False, unique=True) # TODO: Do we really need to store other information about herds here? def __init__(self, name): self.name = name def __repr__(self): return '<%s> - %s' % (self.__class__.__name__, self.name) class Package(db.Model): """Represents packages in the system""" __tablename__ = 'packages' id = db.Column('id', db.Integer, primary_key=True) key = db.Column('key', db.String, nullable=False, unique=True) pkg = db.Column('pkg', db.String, nullable=False) desc = db.Column('desc', db.String) ldesc = db.Column('ldesc', db.String) homepage = db.Column('homepage', db.String) mtime = db.Column('mtime', db.DateTime) cat_id = db.Column('cat_id', db.Integer, db.ForeignKey('categories.id')) qaissues = db.relationship("PkgIssue", backref='qa_issues', \ cascade='all, delete-orphan') devs = db.relationship(Developer, secondary=package_developers, \ backref=db.backref('packages', \ order_by=["%s.key" % __tablename__])) herds = db.relationship(Herd, secondary=package_herds, backref='packages') ebuilds = db.relationship(Ebuild, backref='package', \ cascade='all, delete-orphan', \ collection_class=column_mapped_collection(Ebuild.cpv)) favorites = db.relationship("User", secondary=package_users, backref='favorites') def __init__(self, ebuild_src, mtime=time.time()): self.key = ebuild_src.key self.pkg = ebuild_src.package self.desc = ebuild_src.description self.homepage = ebuild_src.homepage self.mtime = datetime.fromtimestamp(mtime) # shared pkg data (from metadata.xml) self.ldesc = ebuild_src.longdescription self.devs = self._parse_maintainers(ebuild_src) self.herds = self._parse_herds(ebuild_src) def __repr__(self): return '<%s> - %s' % (self.__class__.__name__, self.key) @property def versions(self): """Returns sorted list of versions package has in Portage""" return sorted(list(set(\ [e.version for e in self.ebuilds.values()])), \ compare_version, reverse=True) def _parse_maintainers(self, ebuild_src): """Update package maintainers.""" devs = [] for dev in ebuild_src.maintainers: if not dev.email: continue devs.append(dev.email.strip()) devs = set(devs) if not devs: return [] # Sync developers with database out = [] for dev in Developer.query.filter(Developer.email.in_(devs)).all(): if dev.email in devs: out.append(dev) devs.remove(dev.email) for dev in devs: out.append(Developer(dev)) return out def _parse_herds(self, ebuild_src): """Update package herds.""" herds = [] for herd in ebuild_src.herds: if not herd or herd.strip() is None: herd = 'fix-me' herds.append(herd.strip()) herds = set(herds) if not herds: return [] # Sync herds with database out = [] for herd in Herd.query.filter(Herd.name.in_(herds)).all(): if herd.name in herds: out.append(herd) herds.remove(herd.name) for herd in herds: out.append(Herd(herd)) return out def rename(self, key): """Rename a package and all its ebuilds""" # Look up or create new category cat, pkg = key.split('/') c = Category.query.filter_by(name=cat).first() if not c: c = Category(cat) self.key = key self.pkg = pkg self.category = c for ebuild in self.ebuilds.values(): ebuild.rename(key) def sync(self, ebuild_src, mtime=time.time()): """Update package values from ebuild_src.""" self.__init__(ebuild_src, mtime) class Category(db.Model): """Represents portage categories""" __tablename__ = 'categories' id = db.Column('id', db.Integer, primary_key=True) name = db.Column('name', db.String, nullable=False, unique=True) packages = db.relationship(Package, backref='category', \ cascade='all, delete-orphan', \ collection_class=column_mapped_collection(Package.key)) def __init__(self, name): self.name = name def __repr__(self): return '<%s> "%s"' % (self.__class__.__name__, self.name) class PkgIssue(db.Model): """Package-related issues""" __tablename__ = 'pkg_issues' id = db.Column('id', db.Integer, primary_key=True) plugin = db.Column('plugin', db.String) type = db.Column('type', db.String) data = db.Column('data', db.String) created_on = db.Column('created_on', db.DateTime, default=func.now()) _package = db.Column('package_id', db.Integer, db.ForeignKey('packages.id')) package = db.relationship(Package) def __init__(self, package, plugin, type, data=None): self.package = package self.plugin = plugin self.data = data self.type = type def __repr__(self): return '<%s> - %s : %s' % \ (self.__class__.__name__, self.plugin, self.type) class Setting(db.Model): """Housekeeping table for storing various system settings and info.""" __tablename__ = 'settings' name = db.Column('name', db.String, primary_key=True, unique=True) rawdata = db.Column('data', db.Text) def __init__(self, name, data): self.name = name self.rawdata = json.dumps(data) @property def data(self): return json.loads(self.rawdata) def __repr__(self): return '<%s> - %s' % (self.__class__.__name__, self.name) class User(db.Model): """User model""" __tablename__ = 'users' id = db.Column('id', db.Integer, primary_key=True) openid = db.Column('openid', db.String) email = db.Column('user', db.String, unique=True) apitoken = db.Column('apitoken', db.String) regtoken = db.Column('regtoken', db.String) created_on = db.Column('created_on', db.DateTime, default=func.now()) def __init__(self, email, openid): self.email = email self.openid = openid # API token self.apitoken = None # Registration token self.regtoken = self.generate_token(8) def generate_token(self, length): return ''.join([random.choice(string.ascii_letters + \ string.digits) for x in xrange(length)])