예제 #1
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
 def table(cls):
     """Produces a followers table for the many-to-many relationship."""
     return db.Table(
         f'{cls.__name__.lower()}_followers',
         db.Column(f'{cls.__name__.lower()}_id', db.Integer,
                   db.ForeignKey(f'{cls.__name__.lower()}.id')),
         db.Column('user_id', db.Integer, db.ForeignKey('user.id')))
예제 #2
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
class VoteMixin:
    """A Mixin for votes. This is useful because users can vote on more than
    just Annotations.

    Attributes
    ----------
    delta : int
        The difference the vote applies to the weight of the object.
    timestamp : DateTime
        The timestamp of when the vote was created.
    is_up : bool
        A boolean that indicates whether the vote is up or down.
    """
    delta = db.Column(db.Integer)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow())

    @property
    def is_up(self):
        """A boolean representing the vector of the vote (i.e., True for up,
        False for down).
        """
        return self.delta > 0

    @declared_attr
    def voter_id(cls):
        """The id of the voter."""
        return db.Column(db.Integer, db.ForeignKey('user.id'))

    @declared_attr
    def voter(cls):
        """The voter."""
        # the backref is the name of the class lowercased with the word
        # `ballots` appended
        return db.relationship(
            'User', backref=backref(f'{cls.__name__.lower()}_ballots',
                                    lazy='dynamic'))

    def __repr__(self):
        return f"<{self.voter.displayname} {self.delta} on "

    @declared_attr
    def reputationchange_id(cls):
        return db.Column(db.Integer, db.ForeignKey('reputationchange.id'),
                         default=None)

    @declared_attr
    def repchange(cls):
        return db.relationship('ReputationChange',
                               backref=backref(cls.__name__.lower(),
                                               uselist=False))
예제 #3
0
class EditVote(Base, VoteMixin):
    """A vote on an :class:`Edit`. They are used to determine whether the edit
    should be approved. Only user's with a reputation above a certain threshold
    (or with a certain right) are supposed to be able to apply these (or review
    annotation edits at all). This might, or might not, be the case.
    I will have to add assurances.


    Attributes
    ----------
    edit_id : int
        The id of the :class:`Edit` the vote is applied to.
    edit : :class:`Edit`
        The edit object the vote was applied to.

    The EditVote class also possesses all of the attributes of
    :class:`VoteMixin`.
    """
    edit_id = db.Column(db.Integer, db.ForeignKey('edit.id'), index=True)
    entity = db.relationship('Edit',
                             backref=backref('ballots', lazy='dynamic'))

    def __repr__(self):
        prefix = super().__repr__()
        return f"{prefix}{self.edit}"
예제 #4
0
파일: user.py 프로젝트: Anno-Wiki/icc
class ReputationEnum(Base, EnumMixin):
    """An enum for a ReputationChange.

    Inherits
    --------
    EnumMixin

    Attributes
    ----------
    default_delta : int
        An integer representing the default reputation change value. That is to
        say, the amount by which the event will change the user's reputation.
    """
    default_delta = db.Column(db.Integer, nullable=False)
    entity = db.Column(db.String(128))
    display = db.Column(db.String(128))
예제 #5
0
class CommentVote(Base, VoteMixin):
    """A class that represents a vote on a comment."""
    entity_id = db.Column(db.Integer, db.ForeignKey('comment.id'), index=True)
    entity = db.relationship('Comment')

    def __repr__(self):
        prefix = super().__repr__()
        return f"{prefix}{self.entity}"
예제 #6
0
파일: request.py 프로젝트: Anno-Wiki/icc
class TagRequestVote(Base, VoteMixin):
    tag_request_id = db.Column(db.Integer, db.ForeignKey('tagrequest.id'),
                               index=True)
    entity = db.relationship('TagRequest', backref=backref('ballots'))

    def __repr__(self):
        prefix = super().__repr__()
        return f"{prefix}{self.tag_request}"
예제 #7
0
파일: request.py 프로젝트: Anno-Wiki/icc
class TagRequest(Base, FollowableMixin, VotableMixin):
    __vote__ = TagRequestVote
    __reputable__ = 'requester'
    __approvable__ = 'approve_tag_requests'
    __margin_approvable__ = 'VOTES_FOR_REQUEST_APPROVAL'
    __margin_rejectable__ = 'VOTES_FOR_REQUEST_REJECTION'

    tag = db.Column(db.String(127), index=True)

    weight = db.Column(db.Integer, default=0, index=True)
    approved = db.Column(db.Boolean, default=False, index=True)
    rejected = db.Column(db.Boolean, default=False, index=True)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)

    wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)
    wiki = db.relationship('Wiki', backref=backref('tagrequest', uselist=False))

    requester_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
    requester = db.relationship('User', backref='tag_requests')

    @property
    def url(self):
        return url_for('requests.view_tag_request', request_id=self.id)

    def __init__(self, *args, **kwargs):
        description = kwargs.pop('description', None)
        description = "This wiki is blank." if not description else description
        super().__init__(*args, **kwargs)
        self.wiki = Wiki(body=description, entity_string=str(self))

    def __repr__(self):
        return f'<Request for {self.tag}>'
예제 #8
0
class CommentFlag(Base, FlagMixin):
    """A flag for a comment."""
    entity_id = db.Column(db.Integer, db.ForeignKey('comment.id'), index=True)
    entity = db.relationship('Comment',
                             backref=backref('flags', lazy='dynamic'))

    def __repr__(self):
        return (f'<CommentFlag on {self.entity_id} on'
                f'[{self.entity.annotation.id}]>')
예제 #9
0
파일: user.py 프로젝트: Anno-Wiki/icc
class ReputationChange(Base):
    """An object representing a reputation change event. This thing exists to
    avoid mischanges to the reputation. It's a ledger so we can audit every rep
    change and the user can see the events.

    Attributes
    ----------
    delta : int
        A number representing the change to the user's reputation.
    user_id : int
        The id of the user.
    timestamp : datetime
        The time the reputation change happened
    enum_id : int
        the id of the reputation change enum
    user : :class:`User`
        The user whose reputation was changed.
    enum : :class:`ReputationEnum`
        The enum of the particular rep change.
    """
    delta = db.Column(db.Integer, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)
    enum_id = db.Column(db.Integer,
                        db.ForeignKey('reputationenum.id'),
                        nullable=False)

    user = db.relationship('User', backref='changes')
    enum = db.relationship('ReputationEnum')
    type = association_proxy('enum', 'enum')

    def __repr__(self):
        return (f'<rep change {self.type} on {self.user.displayname} '
                f'{self.timestamp}>')

    @orm.reconstructor
    def __init_on_load__(self):
        # this is cleaner than the entity population of the wiki system, but it
        # relies on the existence of the enum to call on. Since that is
        # necessary, wiki can't use the same method.
        self.entity = getattr(self, self.enum.entity.lower(), None)
예제 #10
0
class WikiEditVote(Base, VoteMixin):
    """A Wiki Edit Vote."""
    edit_id = db.Column(db.Integer,
                        db.ForeignKey('wikiedit.id'),
                        index=True,
                        nullable=False)
    entity = db.relationship('WikiEdit',
                             backref=backref('ballots', passive_deletes=True))

    def __repr__(self):
        prefix = super().__repr__()
        return f"{prefix}{self.edit}"
예제 #11
0
파일: content.py 프로젝트: Anno-Wiki/icc
class WriterConnection(Base):
    """This is a proxy object that connects writers to editions. It resolves
    enum_id to the role of the writer's connection to the edition based on the
    WRITERS tuple.

    Attributes
    ----------
    writer_id : int
        The id of the writer in the connection.
    edition_id : int
        The id of the edition in the connection.
    enum_id : int
        The id of the enumerated role of the connection.
    writer : :class:`Writer`
        The writer object in the connection
    edition : :class:`Edition`
        The edition object in the connection.
    enum : str
        The enumerated string of the writer's role in the connection.
    """
    writer_id = db.Column(db.Integer, db.ForeignKey('writer.id'))
    edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'))
    enum_id = db.Column(db.Integer)

    writer = db.relationship('Writer')
    edition = db.relationship('Edition')

    def __init__(self, *args, **kwargs):
        """Resolves the enum_id to the enum."""
        super().__init__(*args, **kwargs)
        self.enum = WRITERS[self.enum_id]

    @orm.reconstructor
    def __init_on_load__(self):
        """Resolves the enum_id to the enum."""
        self.enum = WRITERS[self.enum_id]

    def __repr__(self):
        return f'<{self.writer.name} was the {self.enum} of {self.edition}>'
예제 #12
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
class EnumMixin:
    """Any enumerated class that has more than 4 types should be use this
    EnumMixin. LineEnum and Right seem to be the biggest examples.

    Attributes
    ----------
    enum : str
        A string for holding an enumerated type.
    """
    enum = db.Column(db.String(128), index=True)

    def __repr__(self):
        return f"<{type(self).__name__} {self.enum}>"

    def __str__(self):
        if hasattr(self, 'display'):
            return self.display
        else:
            return self.enum
예제 #13
0
파일: user.py 프로젝트: Anno-Wiki/icc
class AdminRight(Base, EnumMixin):
    """The class used to represent a user's rights.

    Inherits
    --------
    EnumMixin

    Attributes
    ----------
    min_rep : int
        An integer representing the minimum reputation to authorize a user for
        the right (so we grant certain rights based on the user's reputation).
        If the min_rep is None, the user has to posses the right in their
        `rights`.
    """
    min_rep = db.Column(db.Integer)

    def __repr__(self):
        return f'<Right to {self.enum}>'
예제 #14
0
class AnnotationFlag(Base, FlagMixin):
    """A flagging event for annotations. Uses the AnnotationFlagEnum for
    templating. Will need an update soon to overhaul the flagging system to be
    more public.

    Inherits
    --------
    FlagMixin

    Attributes
    ----------
    annotation_id : int
        The int of the :class:`Annotation` object the flag is applied to.
    entity : :class:`Annotation`
        The actual annotation.
    """
    annotation_id = db.Column(db.Integer,
                              db.ForeignKey('annotation.id'),
                              index=True)
    entity = db.relationship('Annotation',
                             backref=backref('flags', lazy='dynamic'))
예제 #15
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
class Base(db.Model):
    """This Base class does nothing. It is here in case I need to expand
    implement something later. I feel like it's a good early practice.

    Attributes
    ----------
    id : int
        The basic primary key id number of any class.

    Notes
    -----
    The __tablename__ is automatically set to the class name lower-cased.
    There's no need to mess around with underscores, that just confuses the
    issue and makes programmatically referencing the table more difficult.
    """
    __abstract__ = True
    id = db.Column(db.Integer, primary_key=True)

    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()
예제 #16
0
class AnnotationVote(Base, VoteMixin):
    """A class that represents a user's vote on an annotation.

    Attributes
    ----------
    reputationchange_id : int
        The id of the :class:`ReputationChange` object that accompanies the
        :class:`Vote`.
    entity : :class:`Annotation`
        The :class:`Annotation` the vote has been applied to.

    The Vote class also possesses all of the attributes of :class:`VoteMixin`.
    """
    annotation_id = db.Column(db.Integer,
                              db.ForeignKey('annotation.id'),
                              index=True)

    entity = db.relationship('Annotation')

    def __repr__(self):
        prefix = super().__repr__()
        return f"{prefix}{self.entity}"
예제 #17
0
class WikiEdit(Base, EditMixin, VotableMixin):
    """A WikiEdit."""
    __vote__ = WikiEditVote
    __reputable__ = 'editor'
    __approvable__ = 'immediate_wiki_edits'
    __margin_approvable__ = 'VOTES_FOR_APPROVAL'
    __margin_rejectable__ = 'VOTES_FOR_REJECTION'

    entity_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)
    wiki = db.relationship('Wiki', backref=backref('versions', lazy='dynamic'))

    def __repr__(self):
        return f"<WikiEdit on {self.wiki}>"

    @property
    def url(self):
        if self.approved or self.rejected:
            return url_for('main.view_wiki_edit',
                           wiki_id=self.wiki.id,
                           num=self.num)
        else:
            return url_for('admin.review_wiki_edit',
                           wiki_id=self.wiki.id,
                           edit_id=self.id)
예제 #18
0
"""Basic many-to-many tables."""
from icc import db
"""The tags table connects tags to edits."""
tags = db.Table('tags', db.Column('tag_id', db.Integer,
                                  db.ForeignKey('tag.id')),
                db.Column('edit_id', db.Integer, db.ForeignKey('edit.id')))
"""The rights table connects AdminRight to a User."""
rights = db.Table(
    'rights', db.Column('right_id', db.Integer,
                        db.ForeignKey('adminright.id')),
    db.Column('user_id', db.Integer, db.ForeignKey('user.id')))
예제 #19
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
 def enum_id(cls):
     """The id of the enum that this particular flag is typed."""
     return db.Column(db.Integer,
                      db.ForeignKey(f'{cls.__name__.lower()}enum.id'),
                      index=True)
예제 #20
0
파일: content.py 프로젝트: Anno-Wiki/icc
class Line(EnumeratedMixin, SearchableMixin, Base):
    """A line. This is actually a very important class. It is the only
    searchable type so far. Eventually this won't be the case. Then I'll have to
    remember to update this comment. I probably will forget... So if this
    comment is no longer true, please modify it.

    Attributes
    ----------
    __searchable__ : list
        A list of strings that correspond to the attributes that should be
        indexed in elasticsearch. It's defined by :class:`SearchableMixin` so
        see that for more information.
    num : int
        The line number within the edition.
    em : str
        A string corresponding to an enumerated type describing emphasis status.
        See On Emphasis in the wiki
    toc : TOC
        The section the line is in in the TOC. See On the TOC in the Wiki.
    body : str
        The actual text of the line. Processed in my processor.
    text : Text
        An association proxy to the text for ease of reference.
    text_title : str
        The title of the parent text of the edition the line is in.
    edition : Edition
        The edition of the parent text.
    context : list
        A list of all line's surrounding lines (+/- 5 lines)
    annotations : BaseQuery
        An SQLA BaseQuery for all the annotations that contain this line in
        their target. It's a nasty relationship.
    """
    __searchable__ = ['body', 'text_id', 'edition_id', 'writer_id']

    num = db.Column(db.Integer, index=True)
    body = db.Column(db.Text)
    em_id = db.Column(db.Integer)

    toc_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True)
    toc = db.relationship('TOC', backref=backref('lines', lazy='dynamic'))

    edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'), index=True)
    edition = db.relationship('Edition',
                              backref=backref('lines', lazy='dynamic'))

    text = association_proxy('edition', 'text')
    text_id = association_proxy('edition', 'text_id')

    context = db.relationship('Line',
                              primaryjoin='and_(remote(Line.num)<=Line.num+1,'
                              'remote(Line.num)>=Line.num-1,'
                              'remote(Line.edition_id)==Line.edition_id)',
                              foreign_keys=[num, edition_id],
                              remote_side=[num, edition_id],
                              uselist=True,
                              viewonly=True)
    annotations = db.relationship(
        'Annotation',
        secondary='edit',
        primaryjoin='and_(Edit.first_line_num<=foreign(Line.num),'
        'Edit.last_line_num>=foreign(Line.num),'
        'Edit.edition_id==foreign(Line.edition_id),Edit.current==True)',
        secondaryjoin='and_(foreign(Edit.entity_id)==Annotation.id,'
        'Annotation.active==True)',
        foreign_keys=[num, edition_id],
        uselist=True,
        lazy='dynamic')

    @property
    def writers(self):
        return self.edition.writers

    @property
    def writer_id(self):
        out = []
        for tp in self.writers.values():
            for writer in tp:
                out.append(writer.id)
        return out

    @property
    def url(self):
        """The url for the smallest precedence section to read, in is the
        line.
        """
        return self.toc.url

    def __init__(self, *args, **kwargs):
        self.em_id = EMPHASIS.index(kwargs.pop('em'))
        super().__init__(*args, **kwargs)

    @orm.reconstructor
    def __init_on_load__(self):
        """Resolves the em_id to the line's emphasis status."""
        self.em = EMPHASIS[self.em_id]

    def __repr__(self):
        return (f"<l{self.num} {self.edition}>")
예제 #21
0
class Wiki(Base):
    """An actual wiki. It has some idiosyncracies I'm not fond of, notably in
    init_on_load. But it's modelled after my Annotation system.
    """
    entity_string = db.Column(db.String(191), index=True)

    current = db.relationship('WikiEdit',
                              primaryjoin='and_(WikiEdit.entity_id==Wiki.id,'
                              'WikiEdit.current==True)',
                              uselist=False,
                              lazy='joined')
    edits = db.relationship('WikiEdit',
                            primaryjoin='WikiEdit.entity_id==Wiki.id',
                            lazy='dynamic')
    edit_pending = db.relationship(
        'WikiEdit',
        primaryjoin='and_(WikiEdit.entity_id==Wiki.id,'
        'WikiEdit.approved==False, WikiEdit.rejected==False)',
        passive_deletes=False)

    @orm.reconstructor
    def __init_on_load__(self):
        """The primary purpose of this method is to load the entity reference.
        """
        # This is a hack and needs to be improved.
        # Basically, we rely on the backref defined on the entity in order to
        # get the entity. I really want to make this more explicit and less
        # hacky.
        self.entity = list(
            filter(None, [
                self.writer, self.text, self.tag, self.edition,
                self.textrequest, self.tagrequest
            ]))[0]

    def __init__(self, *args, **kwargs):
        """Creating a new wiki also populates the first edit."""
        body = kwargs.pop('body', None)
        body = 'This wiki is currently blank.' if not body else body
        super().__init__(*args, **kwargs)
        self.versions.append(
            WikiEdit(current=True,
                     body=body,
                     approved=True,
                     reason='Initial Version.'))

    def __repr__(self):
        return f'<Wiki HEAD {str(self.entity)} at version {self.current.num}>'

    def edit(self, editor, body, reason):
        """Edit the wiki, creatinga new WikiEdit."""
        edit = WikiEdit(wiki=self,
                        num=self.current.num + 1,
                        editor=editor,
                        body=body,
                        reason=reason)
        db.session.add(edit)
        if editor.is_authorized('immediate_wiki_edits'):
            edit.approved = True
            self.current.current = False
            edit.current = True
            flash("The edit has been applied.")
        else:
            flash("The edit has been submitted for peer review.")
예제 #22
0
파일: mixins.py 프로젝트: Anno-Wiki/icc
 def thrower_id(cls):
     """The id of the user who threw the flag."""
     return db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
예제 #23
0
파일: content.py 프로젝트: Anno-Wiki/icc
class TOC(EnumeratedMixin, Base):
    num = db.Column(db.Integer, index=True)
    precedence = db.Column(db.Integer, default=1, index=True)
    body = db.Column(db.String(200), index=True)
    haslines = db.Column(db.Boolean, index=True, default=False)

    prev_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True)
    prev = db.relationship('TOC',
                           uselist=False,
                           remote_side='TOC.id',
                           foreign_keys=[prev_id],
                           backref=backref('next',
                                           uselist=False,
                                           remote_side='TOC.prev_id'))

    parent_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True)
    parent = db.relationship('TOC',
                             uselist=False,
                             remote_side='TOC.id',
                             foreign_keys=[parent_id],
                             backref=backref('children',
                                             remote_side='TOC.parent_id',
                                             lazy='dynamic'))

    edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'), index=True)
    edition = db.relationship('Edition',
                              backref=backref('toc', lazy='dynamic'))
    text = association_proxy('edition', 'text')

    @property
    def url(self):
        if self.haslines:
            return url_for('main.read',
                           text_url=self.text.url_name,
                           edition_num=self.edition.num,
                           toc_id=self.id)
        return None

    @property
    def section(self):
        parents = self.parents
        nums = [str(p.num) for p in parents]
        nums.append(str(self.num))
        return '.'.join(nums)

    @property
    def parents(self):
        def _parents(toc, struct):
            """Recursive function for the purpose of this method."""
            struct.insert(0, toc)
            if toc.parent:
                return _parents(toc.parent, struct)
            else:
                return struct

        if self.parent:
            return _parents(self.parent, [])
        else:
            return []

    def __repr__(self):
        s = ['<']
        for parent in self.parents:
            s.append(f'{parent.enum} {parent.num} ')
        s.append(f'{self.enum} {self.num} of {self.edition}>')
        return ''.join(s)

    def __str__(self):
        return self.body
예제 #24
0
파일: content.py 프로젝트: Anno-Wiki/icc
class Writer(Base, FollowableMixin, LinkableMixin, SearchLinesMixin):
    """The writer model. This used to be a lot more complicated but has become
    fairly elegant. All historical contributors to the text are writers, be they
    editors, translators, authors, or whatever the heck else we end up coming up
    with (perhaps with the exception of annotator, we'll see).

    Attributes
    ----------
    name : string
        The full name of the writer
    family_name : string
        The family name of the writer. We store this as an actual value in the
        db instead of computing it on the fly because of varying naming
        conventions (e.g., Chinese names). And we need it for sorting purposes.
    birth_date : Date
        The birthdate of the writer.
    death_date : Date
        The deathdate of the writer.
    wiki_id : int
        The id of the descriptive wiki object.
    timestamp : datetime
        The timestamp of when the writer was added to the database. Superfluous.
    followers : BaseQuery
        An SQLA BaseQuery of all of the followers of the writer.
    connections : BaseQuery
        An SQLA BaseQuery of all of the connection objects for the writer (i.e.,
        the writer's relationships to various editions).
    wiki : :class:`Wiki`
        The descriptive wiki object.
    annotations : BaseQuery
        An SQLA BaseQuery for all of the annotations on all of the editions the
        writer is responsible for.
    """
    __linkable__ = 'name'

    name = db.Column(db.String(128), index=True)
    family_name = db.Column(db.String(128), index=True)
    birth_date = db.Column(db.Date, index=True)
    death_date = db.Column(db.Date, index=True)
    wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)

    connections = db.relationship('WriterConnection', lazy='dynamic')
    wiki = db.relationship('Wiki', backref=backref('writer', uselist=False))
    annotations = db.relationship(
        'Annotation',
        secondary='join(WriterConnection, Edition)',
        primaryjoin='Writer.id==WriterConnection.writer_id',
        secondaryjoin='Annotation.edition_id==Edition.id',
        lazy='dynamic')

    @property
    def url(self):
        """The url for the main view page for the writer."""
        return url_for('main.writer', writer_url=self.url_name)

    @property
    def url_name(self):
        """A string that can be used for urls (i.e., it has all the spaces
        replaced with underscores.
        """
        return self.name.replace(' ', '_')

    def __init__(self, *args, **kwargs):
        """Creates a descriptive wiki as well."""
        description = kwargs.pop('description', None)
        description = 'This writer does not have a biography yet.'\
            if not description else description
        super().__init__(*args, **kwargs)
        self.wiki = Wiki(body=description, entity_string=str(self))

    @orm.reconstructor
    def __init_on_load__(self):
        """Creates a dictionary mapping the writer's role to the editions."""
        self.works = defaultdict(list)
        for conn in self.connections.all():
            self.works[conn.enum].append(conn.edition)

    def __repr__(self):
        return f'<Writer: {self.name}>'

    def __str__(self):
        return self.name

    @classmethod
    def get_by_url(cls, name):
        """This is a helper function that takes the output of url_name and
        uses it to get the object that matches it.
        """
        return cls.query.filter_by(name=url.replace('_', ' '))
예제 #25
0
파일: content.py 프로젝트: Anno-Wiki/icc
class Text(Base, FollowableMixin, LinkableMixin, SearchLinesMixin):
    __linkable__ = 'title'
    """The text-object. A text is more a categorical, or philosophical concept.
    In essence, a book can have any number of editions, ranging from different
    translations to re-edited or updated versions (which is more common with
    non-fiction texts, and usually just consists of an added
    preface/introduction).

    Because of this dynamic, a text can is just a shell with a wiki that
    editions point to. Editions are the heart of the program. But the Text has a
    primary edition to which all annotations should be directed for a work
    unless they regard textual issues that differ between translations or
    editions.

    Attributes
    ----------
    title : string
        The title string of the object
    sort_title : string
        A string by which these objects will be sorted (i.e., so that it doesn't
        contain modifier words like "the" or "a".
    wiki_id : int
        The id of the wiki object.
    published : Date
        A date corresponding to the publication date of the first edition of the
        text irl.
    timestamp : DateTime
        This is just a timestamp marking when the text was added to the
        database. It's really unnecessary, tbh. Eventually to be eliminated.
    followers : BaseQuery
        An SQLA BaseQuery object for all the followers of the text.
    wiki : :class:`Wiki`
        A wiki object describing the text.
    editions : BaseQuery
        An SQLA BaseQuery object for all the editions pointing to this text.
    primary : :class:`Edition`
        An edition object that represents the primary edition of the text (the
        one we direct all annotations to unless they're about textual issues,
        etc.)
    url : string
        A url string for the main view page of the text object.
    url_name : string
        A string that can be used in a url for the text_url parameter.
    """
    @classmethod
    def get_by_url(cls, url):
        """This is a helper function that takes the output of url_name and
        uses it to get the object that matches it.
        """
        return cls.query.filter_by(title=url.replace('_', ' '))

    @classmethod
    def get_object_by_link(cls, name):
        """Get the object by the name."""
        if not cls.__linkable__:
            raise AttributeError("Class does not have a __linkable__ "
                                 "attribute.")
        obj = cls.query.filter(getattr(cls, cls.__linkable__) == name).first()
        return obj

    @classmethod
    def link(cls, name):
        """Override the LinkableMixin link method."""
        idents = name.split(':')
        obj = cls.get_object_by_link(idents[0])
        if not obj:
            return name
        else:
            if len(idents) == 1:
                return f'<a href="{obj.url}">{name}</a>'
            else:
                try:
                    return obj.link_edition(idents[1:])
                except AttributeError:
                    return name

    def link_edition(self, idents):
        """This is specifically to link other editions and line numbers."""
        edition_num = idents[0] if idents[0].isdigit() else 1
        edition = self.editions.filter_by(num=edition_num).first()
        if not edition:
            raise AttributeError("No edition.")
        line_nums = None
        for ident in idents:
            if 'l' in ident:
                line_nums = ident
        if line_nums:
            url = url_for('main.lines',
                          text_url=self.url_name,
                          edition_num=edition_num,
                          nums=line_nums)
            return f"<a href=\"{url}\">{str(edition)} {line_nums}</a>"
        else:
            return f"<a href=\"{edition.url}\">{str(edition)}</a>"

    title = db.Column(db.String(128), index=True)
    sort_title = db.Column(db.String(128), index=True)
    wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)
    published = db.Column(db.Date)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow())

    wiki = db.relationship('Wiki', backref=backref('text', uselist=False))
    editions = db.relationship('Edition', lazy='dynamic')
    primary = db.relationship(
        'Edition',
        primaryjoin='and_(Edition.text_id==Text.id,Edition.primary==True)',
        uselist=False)

    @property
    def url(self):
        """This returns the actual internally-resolved url for the text's main
        view page.
        """
        return url_for('main.text', text_url=self.url_name)

    @property
    def url_name(self):
        """Returns the name of the object (title) translated in to a
        url-utilizable string (i.e., no spaces).

        Notes
        -----
        This method right now is very simple. The issue might become more
        complex when we have works with titles that have punctuation marks in
        them. eventually we might have to modify this method to translate those
        characters into url-acceptable characters (e.g., escape them).
        """
        return self.title.replace(' ', '_')

    def __init__(self, *args, **kwargs):
        """This init method creates a wiki for the object with the supplied
        description.
        """
        description = kwargs.pop('description', None)
        description = "This wiki is blank." if not description else description
        super().__init__(*args, **kwargs)
        self.wiki = Wiki(body=description, entity_string=str(self))

    def __repr__(self):
        return f'<Text {self.id}: {self.title}>'

    def __str__(self):
        return self.title
예제 #26
0
파일: content.py 프로젝트: Anno-Wiki/icc
class Edition(Base, FollowableMixin, SearchLinesMixin):
    """The Edition model. This is actually more central to the app than the Text
    object. The edition has all of the writer connections, annotations, and
    lines connected to it.

    Attributes
    ----------
    num : int
        The edition number of the edition. I.e., edition 1, 2, etc.
    primary : bool
        A boolean switch to represent whether or not the specific edition is the
        primary edition for it's parent text. Only one *should* be primary. If
        more than one is primary, this is an error and needs to be corrected.
    published : DateTime
        The publication date of the specific edition.
    timestamp : DateTime
        Simple timestamp for when the Edition was created.
    connections : BaseQuery
        A filterable BaseQuery of WriterConnections.
    text_title : str
        A proxy for the title of the text. Mostly used so that the Annotation
        can query it easily with it's own association_proxy.
    tochide : boolean
        A flag for disabling the showing of the toc dispay enum because the toc
        body is complete on it's own.
    verse : boolean
        A flag for disabling the line concatenation that happens on cell phones.
        It will eventually become (either) more useful, or less. Unsure yet.
    """
    _title = db.Column(db.String(235), default=None)
    num = db.Column(db.Integer, default=1)
    text_id = db.Column(db.Integer, db.ForeignKey('text.id'))
    primary = db.Column(db.Boolean, default=False)
    wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)
    published = db.Column(db.DateTime)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow())

    tochide = db.Column(db.Boolean, default=True)
    verse = db.Column(db.Boolean, default=False)

    annotations = db.relationship('Annotation', lazy='dynamic')
    connections = db.relationship('WriterConnection', lazy='dynamic')
    wiki = db.relationship('Wiki', backref=backref('edition', uselist=False))
    text = db.relationship('Text')
    text_title = association_proxy('text', 'title')

    @property
    def url(self):
        """A string title of the edition. Basically consists of the parent
        text's title and an asterisk for the primary edition or 'Edition #'.
        """
        return url_for('main.edition',
                       text_url=self.text.url_name,
                       edition_num=self.num)

    @property
    def title(self):
        """Returns the title representation string of the edition."""
        if self._title:
            return f'{self._title}*' if self.primary else self._title
        return (f'{self.text_title}*'
                if self.primary else f"{self.text_title} - Ed. #{self.num}")

    @property
    def edition_title(self):
        """A string for the edition similar to title but *not* including the
        parent text's title.
        """
        if self._title:
            return f'{self._title}*' if self.primary else self._title
        return (f"Edition #{self.num} - Primary"
                if self.primary else f"Edition #{self.num}")

    @property
    def tocstruct(self):
        byparentid = defaultdict(list)
        for toc in self.toc:
            byparentid[toc.parent_id].append(toc)
        return byparentid

    def __init__(self, *args, **kwargs):
        """Creates a wiki for the edition with the provided description."""
        description = kwargs.pop('description', None)
        description = 'This wiki is blank.' if not description else description
        super().__init__(*args, **kwargs)
        self.wiki = Wiki(body=description, entity_string=str(self))

    @orm.reconstructor
    def __init_on_load__(self):
        """Creates a defaultdict of lists of writers based on their connection
        type (e.g., author, editor, translator, etc.)
        """
        self._create_writers()

    def _create_writers(self):
        """This creates the _writers attr. It's abstracted for use by writers()
        and __init_on_load__().
        """
        self._writers = defaultdict(list)
        for conn in self.connections.all():
            self._writers[conn.enum].append(conn.writer)

    @property
    def writers(self):
        """This by using a property to access the writerslist I ensure that it
        will always exist before it is accessed. This prevents the error I get
        when first creating this thing where I try to access writers and it
        doesn't exist when indexing on a first run through.
        """
        if not hasattr(self, '_writers'):
            self._create_writers()
        return self._writers

    def __repr__(self):
        return f'<{self.title}>'

    def __str__(self):
        return self.title

    def get_lines(self, nums):
        if len(nums) >= 2:
            line_query = self.lines.filter(Line.num >= nums[0],
                                           Line.num <= nums[-1])
        else:
            line_query = self.lines.filter(Line.num == nums[0])
        return line_query
예제 #27
0
class Edit(Base, EditMixin, VotableMixin):
    """The Edit class, which represents the current state of an
    :class:`Annotation`. An annotation object is just a HEAD, like in git. Or,
    rather, a tag? I can't remember how git's model works, but essentially, the
    annotation object just serves as a pointer to it's current edit.

    Attributes
    ----------
    edition_id : int
        The id of the edition to which the annotation is applied. This column is
        *technically* redundant, but it simplifies some operations. I may
        eventually (probably sooner than later, actually, now that I know about
        SQLA's association_proxy), eliminate this column.
    entity_id : int
        This points back at the edit's annotation. I do not have a clue why I
        titled it entity_id. This seems like it could, and should, change. It
        used to be called the pointer_id. I guess entity_id is a step up from
        that? I'll make it annotation_id after I finish documenting this and
        working through other problems.
    first_line_num : int
        This corresponds to the :class:`Line` `num` that corresponds to the
        first line of the target of the annotation's current version, *not* it's
        id. This is because I do not want the annotation to point to an abstract
        id, but to a line in a book. Because I absolutely do not want to make
        this fragile.  I want a robust ability to export the annotations. This
        may be silly, and eventually someone can convince me it is, especially
        given association_proxies, etc. But for now it's staying like this.
    last_line_num : int
        The same as the first_line_num, but the last of the target.
    first_char_idx : int
        The string-index of the first character of the first line which
        corresponds to the character-by-character target of the annotation. I am
        not currently storing anything but 0 here. Once we write the JavaScript
        corresponding to char-by-char annotation target selection, this will
        change.

        Note: this method seems fragile to me, and I am nervous about it. If I
        ever begin to edit lines, these could become de-indexed improperly. Then
        we could get out-of-bounds exceptions all over the place and fail to
        render pages. I am interested in a more robust solution to this.
    last_char_idx : int
        The *last* character of the last line of the target of the annotation.
        Read first_char_idx for an explanation.

        Note: I would like for this to always be a negative number. This could
        *also* result in problems. But I feel like it would be better to reverse
        index the last character than to forward index the last character. It
        *feels* more robust.
    edition : :class:`Edition`
        The edition object the annotation is applied to. This will become an
        association_proxy when I get off my fat behind and take care of that.
        Probably pretty soon.
    annotation : :class:`Annotation`
        The annotation the edit is applied to.
    lines : list
        A list of all of the lines that are the target of the edit.
    context : list
        A list of all the lines that are the target of the edit *plus* five
        lines on either side of the first and last lines of the target lines.
    """
    __vote__ = EditVote
    __reputable__ = 'editor'
    __approvable__ = 'immediate_edits'
    __margin_approvable__ = 'VOTES_FOR_APPROVAL'
    __margin_rejectable__ = 'VOTES_FOR_REJECTION'

    edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'), index=True)
    edition = db.relationship('Edition')
    toc_id = db.Column(db.Integer, db.ForeignKey('toc.id'), index=True)
    toc = db.relationship('TOC')
    entity_id = db.Column(db.Integer,
                          db.ForeignKey('annotation.id'),
                          index=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow())

    first_line_num = db.Column(db.Integer, db.ForeignKey('line.num'))
    last_line_num = db.Column(db.Integer,
                              db.ForeignKey('line.num'),
                              index=True)
    first_char_idx = db.Column(db.Integer)
    last_char_idx = db.Column(db.Integer)

    annotation = db.relationship('Annotation')
    tags = db.relationship('Tag', secondary='tags')
    lines = db.relationship(
        'Line',
        primaryjoin='and_(Line.num>=Edit.first_line_num,'
        'Line.num<=Edit.last_line_num, Line.toc_id==Edit.toc_id)',
        uselist=True,
        foreign_keys=[toc_id, first_line_num, last_line_num])
    context = db.relationship(
        'Line',
        primaryjoin=f'and_(Line.num>=Edit.first_line_num-{CONTEXT},'
        f'Line.num<=Edit.last_line_num+{CONTEXT},'
        'Line.toc_id==Edit.toc_id)',
        uselist=True,
        viewonly=True,
        foreign_keys=[toc_id, first_line_num, last_line_num])

    @property
    def first_line(self):
        return self.lines[0]

    @orm.reconstructor
    def __init_on_load__(self):
        """Create the hash_id that recognizes when an edit differs from it's
        previous version to prevent dupe edits.
        """
        s = (f'{self.first_line_num},{self.last_line_num},'
             f'{self.first_char_idx},{self.last_char_idx},'
             f'{self.body},{self.tags}')
        self.hash_id = sha1(s.encode('utf8')).hexdigest()

    def __init__(self, *args, **kwargs):
        """This init checks to see if the first line and last line aren't
        reversed, because that can be a problem. I should probably make it do
        the same for the characters.

        It also generates the hash_id to check against the previous edit.
        """
        super().__init__(*args, **kwargs)
        if self.first_line_num > self.last_line_num:
            tmp = self.last_line_num
            self.last_line_num = self.first_line_num
            self.first_line_num = tmp
        s = (f'{self.first_line_num},{self.last_line_num},'
             f'{self.first_char_idx},{self.last_char_idx},'
             f'{self.body},{self.tags}')
        self.hash_id = sha1(s.encode('utf8')).hexdigest()

    def __repr__(self):
        prefix = super().__repr__()
        return f"<{prefix} {self.annotation}>"

    def get_hl(self):
        """This method is supposed to return the specific lines of the target
        *and* truncate the first and last line based on the actual
        character-by-character targetting which is not in effect yet. It is,
        therefore, currently useless.
        """
        lines = self.lines
        if self.first_line_num == self.last_line_num:
            lines[0].line = lines[0].line[self.first_char_idx:self.
                                          last_char_idx]
        else:
            lines[0].line = lines[0].line[self.first_char_idx:]
            lines[-1].line = lines[-1].line[:self.last_char_idx]
        return lines

    @property
    def url(self):
        if self.approved:
            return url_for('main.view_edit',
                           annotation_id=self.annotation.id,
                           num=self.num)
        else:
            return url_for('admin.review_edit',
                           annotation_id=self.annotation.id,
                           edit_id=self.id)
예제 #28
0
class Comment(Base, VotableMixin):
    """A class representing comments on annotations.

    Attributes
    ----------
    poster_id : int
        The id of the user who posted the comment.
    annotation_id : int
        The id of the annotation the comment is applied to.
    parent_id : int
        The id of the parent comment (None if it is a top level comment, i.e., a
        thread).
    depth : int
        The depth level of the comment in a thread.
    weight : int
        The weight of the comment.
    body : str
        The body of the comment.
    timestamp : datetime
        When the comment was posted, in the utc timezone.
    poster : User
        The user who posted the comment.
    annotation : Annotation
        The annotation the comment is on.
    parent : Comment
        The parent of the comment.
    children : BaseQuery
        The immediate children of the comment.
    """
    __vote__ = CommentVote
    __reputable__ = 'poster'

    poster_id = db.Column(db.Integer,
                          db.ForeignKey('user.id'),
                          index=True,
                          nullable=False)
    annotation_id = db.Column(db.Integer,
                              db.ForeignKey('annotation.id'),
                              index=True,
                              nullable=False)
    parent_id = db.Column(db.Integer,
                          db.ForeignKey('comment.id'),
                          index=True,
                          default=None)
    depth = db.Column(db.Integer, default=0)
    weight = db.Column(db.Integer, default=0)
    body = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow())

    poster = db.relationship('User',
                             backref=backref('comments', lazy='dynamic'))
    annotation = db.relationship('Annotation',
                                 backref=backref('comments', lazy='dynamic'))
    parent = db.relationship('Comment',
                             remote_side='Comment.id',
                             uselist=False,
                             backref=backref('children',
                                             remote_side=[parent_id],
                                             lazy='dynamic'))

    @property
    def url(self):
        return url_for('main.comments', annotation_id=self.annotation_id)

    def __repr__(self):
        return f'<Comment {self.id} on [{self.annotation_id}]>'
예제 #29
0
class Annotation(Base, FollowableMixin, LinkableMixin, VotableMixin):
    """And now, the moment you've been waiting for, the star of the show: the
    main Annotation data class.

    Attributes
    ----------
    weight : int
        The weight of the annotation in upvotes/downvotes.
    timestamp : datetime
        The UTC date time when the annotation was first created.
    locked : bool
        The flag that indicates whether the annotation is locked from editing.
    active : bool
        The flag that indicates the annotation has been deactivated from
        viewing.
    ballots : BaseQuery
        An SQLA BaseQuery of all of the ballots that have been cast for this
        annotation.
    annotator : :class`User`
        The user object of the user who annotated the annotation to begin with.
    first_line : :class:`Line`
        The first line of the target of the annotation. I don't know why this is
        here. I don't know what I use it for. TBD.
    edition : :class:`Edition`
        The edition object the annotation is applied to.
    text : :class:`Text`
        The text object the edition belongs to upon which the annotation is
        annotated.
    HEAD : :class:`Edit`
        The edit object that is the current version of the annotation object. I
        want to eventually change this to current, and it seems like it would
        not be hard. But I am hesitant to do it because then it would not be
        unique, and it would no longer be easy to change. All it requires to
        change is a `find . -type f | xargs sed -i 's/HEAD/current/g'` command
        in the root directory of the project and the change would be complete.
        But since there are other objects which use the current designation
        (namely, :class:`Wiki`), once this command is applied and committed,
        there's really no going back without manually finding it. So for now,
        this is where it's staying.
    history : list
        All of the edits which have been approved, dynamically.
    all_edits : list
        All of the edits. All of them. Like, every one of them, period.
    edit_pending : list
        A list of edits which are neither approved, nor rejected. Essentially,
        this just serves to indicate as a bool-like list object that there is an
        edit pending, because the system is designed never to allow more than
        one edit pending at a time. This is, in fact, false, and I need to go
        through the system and fix the bug whereby a user could surreptitiously
        cause a race condition and have two edits submitted at the same time.
    lines : list
        A list of all of the lines that are the target of the annotation.
    context : list
        A list of all the lines that are the target of the annotation *plus*
        five lines on either side of the first and last lines of the target
        lines.
    flag_history : list
        A list of all of the :class:`AnnotationFlag`s which have been applied to
        the annotation.
    active_flags : list
        A list of all of the flags that are currently active on the annotation.

    Notes
    -----
    The four edit lists are redundant and will need to be eliminated and
    whittled down. There should only be three: the approved edit history, the
    rejected edits, and the pending edits. There's no reason for anything else.
    Even the rejected edits are actually useless. I think I'll leave that out.
    Never is better than right now.

    The flags will also have to be refined. I want to make the flag system more
    public-facing, like Wikipedia's warning templates.

    And finally, the lines could possibly be changed. The context, for instance,
    might be prior-context and posterior-context, or something of that nature,
    instead of packing the same lines into the same list. Perhaps not. TBD.
    """
    __vote__ = AnnotationVote
    __reputable__ = 'annotator'
    __linkable__ = 'id'

    annotator_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
    edition_id = db.Column(db.Integer, db.ForeignKey('edition.id'), index=True)
    weight = db.Column(db.Integer, default=0)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    locked = db.Column(db.Boolean, index=True, default=False)
    active = db.Column(db.Boolean, default=True)

    ballots = db.relationship('AnnotationVote', lazy='dynamic')
    annotator = db.relationship('User')

    @property
    def first_line(self):
        return self.HEAD.first_line

    edition = db.relationship('Edition')
    text = db.relationship('Text',
                           secondary='edition',
                           backref=backref('annotations', lazy='dynamic'),
                           uselist=False)

    # relationships to `Edit`
    HEAD = db.relationship('Edit',
                           primaryjoin='and_(Edit.current==True,'
                           'Edit.entity_id==Annotation.id)',
                           uselist=False)

    # history is used for annotation edit history
    history = db.relationship(
        'Edit',
        primaryjoin='and_(Edit.entity_id==Annotation.id, Edit.approved==True)',
        lazy='dynamic')
    # all_edits is used primarily for the cascade delete
    all_edits = db.relationship('Edit',
                                primaryjoin='Edit.entity_id==Annotation.id',
                                lazy='dynamic')
    # edit_pending is used primarily as a falsey boolean
    edit_pending = db.relationship(
        'Edit',
        primaryjoin='and_(Edit.entity_id==Annotation.id, Edit.approved==False, '
        'Edit.rejected==False)')

    # relationships to `Line`
    lines = db.relationship(
        'Line',
        secondary='edit',
        primaryjoin='and_(Annotation.id==Edit.entity_id,Edit.current==True)',
        secondaryjoin='and_(Line.num>=Edit.first_line_num,'
        'Line.num<=Edit.last_line_num,Line.edition_id==Annotation.edition_id)',
        viewonly=True,
        uselist=True)
    context = db.relationship(
        'Line',
        secondary='edit',
        primaryjoin='and_(Annotation.id==Edit.entity_id,Edit.current==True)',
        secondaryjoin=f'and_(Line.num>=Edit.first_line_num-{CONTEXT},'
        f'Line.num<=Edit.last_line_num+{CONTEXT},'
        'Line.edition_id==Annotation.edition_id)',
        viewonly=True,
        uselist=True)

    # Relationships to `Flag`
    flag_history = db.relationship(
        'AnnotationFlag',
        primaryjoin='Annotation.id==AnnotationFlag.annotation_id',
        lazy='dynamic')
    active_flags = db.relationship(
        'AnnotationFlag',
        primaryjoin='and_(Annotation.id==AnnotationFlag.annotation_id,'
        'AnnotationFlag.resolver_id==None)')

    def __str__(self):
        return f'[{self.id}]'

    @property
    def url(self):
        return url_for('main.annotation', annotation_id=self.id)

    def __init__(self,
                 *ignore,
                 edition,
                 annotator,
                 toc,
                 locked=False,
                 fl,
                 ll,
                 fc,
                 lc,
                 body,
                 tags):
        """This init method creates the initial :class:`Edit` object. This
        reduces friction in creating annotations.
        """
        params = [edition, annotator, fl, ll, fc, lc, body, tags]
        if ignore:
            raise TypeError("Positional arguments not accepted.")
        elif None in params:
            raise TypeError("Keyword arguments cannot be None.")
        elif not type(tags) == list:
            raise TypeError("Tags must be a list of tags.")
        super().__init__(edition=edition, annotator=annotator, locked=locked)
        current = Edit(annotation=self,
                       approved=True,
                       current=True,
                       editor=annotator,
                       edition=edition,
                       first_line_num=fl,
                       last_line_num=ll,
                       toc=toc,
                       first_char_idx=fc,
                       last_char_idx=lc,
                       body=body,
                       tags=tags,
                       num=0,
                       reason="initial version")
        db.session.add(current)
        self.HEAD = current

    def edit(self, *ignore, editor, reason, fl, ll, fc, lc, body, tags):
        """This method creates an edit for the annotation. It is much more
        transparent than creating the edit independently.
        """
        params = [editor, reason, fl, ll, fc, lc, body, tags]
        if ignore:
            raise TypeError("Positional arguments not accepted.")
        elif None in params:
            raise TypeError("Keyword arguments cannot be None.")
        elif not isinstance(tags, list):
            raise TypeError("Tags must be a list of tags.")
        elif not all(isinstance(tag, Tag) for tag in tags):
            raise TypeError("Tags must be a list of tag objects.")
        edit = Edit(toc=self.HEAD.toc,
                    edition=self.edition,
                    editor=editor,
                    num=self.HEAD.num + 1,
                    reason=reason,
                    annotation=self,
                    first_line_num=fl,
                    last_line_num=ll,
                    first_char_idx=fc,
                    last_char_idx=lc,
                    body=body,
                    tags=tags)
        if edit.hash_id == self.HEAD.hash_id:
            flash("Your suggested edit is no different from the previous "
                  "version.")
            return False
        elif editor == self.annotator or\
                editor.is_authorized('immediate_edits'):
            edit.approved = True
            self.HEAD.current = False
            edit.current = True
            flash("Edit approved.")
        else:
            flash("Edit submitted for review.")
        db.session.add(edit)
        return True

    def up_power(self, voter):
        """An int that represents the user's current upvote power.

        This is currently set to 10log10 of the user's reputation, floored at 1.
        """
        if not current_app.config['LOG_POWER']:
            return 1
        if voter.reputation <= 1:
            return 1
        else:
            return int(10 * log10(voter.reputation))

    def down_power(self, voter):
        """An int of the user's down power. This is simply half of the user's up
        power, but at least one.
        """
        if not current_app.config['LOG_POWER']:
            return -1
        power = self.up_power(voter)
        if power / 2 <= 1:
            return -1
        else:
            return -int(power)
예제 #30
0
class Tag(Base, FollowableMixin, LinkableMixin):
    __linkable__ = 'tag'
    """A class representing tags.

    Attributes
    ----------
    tag : str
        The name of the tag
    locked : bool
        The locked status of the tag (i.e., whether it is available for ordinary
        users to apply to their annotations).
    wiki_id : int
        The id of the tag's wiki.
    wiki : :class:`Wiki`
        The tag's :class:`Wiki` object.
    annotations : :class:`BaseQuery`
        A :class:`BaseQuery` object of all of the annotations which currently
        have this tag applied to them.
    """
    @classmethod
    def intersect(cls, tags):
        """Get the annotations at the intersection of multiple tags.

        Parameters
        ----------
        tags : tuple
            A tuple of strings corresponding to the names of the tags to be
            intersected.

        Returns
        -------
        :class:`BaseQuery`
            A :class:`BaseQuery` object that can be used to get all of the
            annotations.

        Raises
        ------
        TypeError
            If the first argument is not a tuple.
        TypeError
            If the elements of the tuple are not all strings.
        """
        if not isinstance(tags, tuple):
            raise TypeError('Tags argument must be a tuple of strings.')
        if not all(isinstance(tag, str) for tag in tags):
            raise TypeError("The tags tuple must consist of only strings.")

        queries = []
        for tag in tags:
            queries.append(cls.query.filter_by(tag=tag).first().annotations)
        query = queries[0].intersect(*queries[1:])
        return query

    @classmethod
    def union(cls, tags):
        """Get the annotations at the union of multiple tags.

        Parameters
        ----------
        tags : tuple
            A tuple of strings corresponding to the names of the tags to be
            unioned.

        Returns
        -------
        BaseQuery
            A :class:`BaseQuery` object that can be used to get all of the
            annotations.

        Raises
        ------
        TypeError
            If the first argument is not a tuple.
        TypeError
            If the elements of the tuple are not all strings.
        """
        if not isinstance(tags, tuple):
            raise TypeError('Tags argument must be a tuple of strings.')
        if not all(isinstance(tag, str) for tag in tags):
            raise TypeError("The tags tuple must consist of only strings.")

        queries = []
        for tag in tags:
            queries.append(cls.query.filter_by(tag=tag).first().annotations)
        query = queries[0].union(*queries[1:])
        return query

    tag = db.Column(db.String(128), index=True, unique=True)
    locked = db.Column(db.Boolean, default=False)
    wiki_id = db.Column(db.Integer, db.ForeignKey('wiki.id'), nullable=False)

    wiki = db.relationship('Wiki', backref=backref('tag', uselist=False))
    annotations = db.relationship(
        'Annotation',
        secondary='join(tags, Edit, and_(tags.c.edit_id==Edit.id,'
        'Edit.current==True))',
        primaryjoin='Tag.id==tags.c.tag_id',
        secondaryjoin='and_(Edit.entity_id==Annotation.id,'
        'Annotation.active==True)',
        lazy='dynamic')

    @property
    def url(self):
        """The url for the main view page for the tag."""
        return url_for('main.tag', tag=self.tag)

    def __init__(self, *args, **kwargs):
        """Creation of a tag also creates a :class:`Wiki`."""
        description = kwargs.pop('description', None)
        description = ("This tag has no description yet."
                       if not description else description)
        super().__init__(*args, **kwargs)
        self.wiki = Wiki(body=description, entity_string=str(self))

    def __repr__(self):
        return f'<Tag {self.id}: {self.tag}>'

    def __str__(self):
        return f'<div class="tag">{self.tag}</div>'