Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CfP tags #1295

Merged
merged 7 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/base/dev/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from models.volunteer.shift import Shift
from models.volunteer.role import Role

from apps.cfp.tasks import create_tags

from . import dev_cli
from .fake import FakeDataGenerator

Expand All @@ -20,6 +22,7 @@ def dev_data(ctx):
ctx.invoke(fake_data)
ctx.invoke(volunteer_data)
ctx.invoke(volunteer_shifts)
ctx.invoke(create_tags)


@dev_cli.command("cfp_data")
Expand Down
25 changes: 25 additions & 0 deletions apps/cfp/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from main import db
from models.cfp import Proposal, TalkProposal, WorkshopProposal, InstallationProposal
from models.cfp_tag import Tag, DEFAULT_TAGS
from models.user import User
from apps.cfp_review.base import send_email_for_proposal
from ..common.email import from_email
Expand Down Expand Up @@ -125,3 +126,27 @@ def email_reserve():
send_email_for_proposal(
proposal, reason="reserve-list", from_address=from_email("SPEAKERS_EMAIL")
)


@cfp.cli.command(
"create_tags",
help=f"Add tags to the database. Defaults are {DEFAULT_TAGS}.",
)
@click.argument("tags_to_create", nargs=-1)
def create_tags(tags_to_create):
"""Upset tag list"""
if not tags_to_create:
tags_to_create = DEFAULT_TAGS

tags_created = 0
for tag in tags_to_create:
if Tag.query.filter_by(tag=tag).all():
app.logger.info(f"'{tag}' already exists, skipping.")
continue

db.session.add(Tag(tag))
tags_created += 1
app.logger.info(f"'{tag}' added to session.")

db.session.commit()
app.logger.info(f"Successfully created {tags_created} new tags.")
68 changes: 60 additions & 8 deletions apps/cfp_review/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,23 @@
from main import db, external_url
from .majority_judgement import calculate_max_normalised_score
from models.cfp import (
Proposal,
LightningTalkProposal,
CFPMessage,
CFPVote,
Venue,
InvalidVenueException,
MANUAL_REVIEW_TYPES,
get_available_proposal_minutes,
ROUGH_LENGTHS,
get_days_map,
DEFAULT_VENUES,
EVENT_SPACING,
FavouriteProposal,
get_available_proposal_minutes,
get_days_map,
HUMAN_CFP_TYPES,
InvalidVenueException,
LightningTalkProposal,
MANUAL_REVIEW_TYPES,
ORDERED_STATES,
Proposal,
ROUGH_LENGTHS,
Venue,
)
from models.cfp_tag import Tag
from models.user import User
from models.purchase import Ticket
from .forms import (
Expand Down Expand Up @@ -138,6 +141,22 @@ def filter_proposal_request():
)
)

tags = request.args.getlist("tags")
if "untagged" in tags:
if len(tags) > 1:
flash("'untagged' in 'tags' arg, other tags ignored")
filtered = True
# join(..outer=True) == left outer join
proposal_query = proposal_query.join(Proposal.tags, isouter=True).filter(
Tag.id.is_(None)
)

elif tags:
filtered = True
proposal_query = proposal_query.join(Proposal.tags).filter(
Proposal.tags.any(Tag.tag.in_(tags))
)

sort_dict = get_proposal_sort_dict(request.args)
proposal_query = proposal_query.options(joinedload(Proposal.user)).options(
joinedload("user.owned_tickets")
Expand All @@ -159,12 +178,18 @@ def proposals():
if "reverse" in non_sort_query_string:
del non_sort_query_string["reverse"]

tag_counts = {t.tag: [0, len(t.proposals)] for t in Tag.query.all()}
for prop in proposals:
for t in prop.tags:
tag_counts[t.tag][0] = tag_counts[t.tag][0] + 1

return render_template(
"cfp_review/proposals.html",
proposals=proposals,
new_qs=non_sort_query_string,
filtered=filtered,
total_proposals=Proposal.query.count(),
tag_counts=tag_counts,
)


Expand Down Expand Up @@ -295,6 +320,8 @@ def log_and_close(msg, next_page, proposal_id=None):
else:
raise Exception("Unknown cfp type {}".format(prop.type))

form.tags.choices = [(t.tag, t.tag) for t in Tag.query.order_by(Tag.tag).all()]

# Process the POST
if form.validate_on_submit():
try:
Expand Down Expand Up @@ -343,6 +370,7 @@ def log_and_close(msg, next_page, proposal_id=None):
form.state.data = prop.state
form.title.data = prop.title
form.description.data = prop.description
form.tags.data = [t.tag for t in prop.tags]
form.requirements.data = prop.requirements
form.length.data = prop.length
form.notice_required.data = prop.notice_required
Expand Down Expand Up @@ -1173,4 +1201,28 @@ def lightning_talks():
)


@cfp_review.route("/proposals-summary")
@schedule_required
def proposals_summary():
counts_by_tag = {t.tag: len(t.proposals) for t in Tag.query.all()}
counts_by_tag["untagged"] = 0

counts_by_type = {t: 0 for t in HUMAN_CFP_TYPES}
counts_by_state = {s: 0 for s in ORDERED_STATES}

for prop in Proposal.query.all():
counts_by_type[prop.type] += 1
counts_by_state[prop.state] += 1

if not prop.tags:
counts_by_tag["untagged"] += 1

return render_template(
"cfp_review/proposals_summary.html",
counts_by_tag=counts_by_tag,
counts_by_type=counts_by_type,
counts_by_state=counts_by_state,
)


from . import venues # noqa
7 changes: 5 additions & 2 deletions apps/cfp_review/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
StringField,
FieldList,
FormField,
RadioField,
SelectField,
TextAreaField,
BooleanField,
IntegerField,
FloatField,
SelectMultipleField,
)
from wtforms.validators import DataRequired, Optional, NumberRange, ValidationError

from models.cfp import Venue, ORDERED_STATES
from models.cfp_tag import Tag
from ..common.forms import Form, HiddenIntegerField, EmailField

from dateutil.parser import parse as parse_date
Expand All @@ -24,6 +25,7 @@ class UpdateProposalForm(Form):
state = SelectField("State", choices=[(s, s) for s in ORDERED_STATES])
title = StringField("Title", [DataRequired()])
description = TextAreaField("Description", [DataRequired()])
tags = SelectMultipleField("Tags")
requirements = TextAreaField("Requirements")
length = StringField("Length")
notice_required = SelectField(
Expand All @@ -50,7 +52,7 @@ class UpdateProposalForm(Form):
telephone_number = StringField("Telephone")
eventphone_number = StringField("On-site extension")
may_record = BooleanField("May record")
needs_laptop = RadioField(
needs_laptop = SelectField(
"Needs laptop",
choices=[
(0, "Is providing their own laptop"),
Expand Down Expand Up @@ -94,6 +96,7 @@ def validate_allowed_times(self, field):
def update_proposal(self, proposal):
proposal.title = self.title.data
proposal.description = self.description.data
proposal.tags = Tag.parse_serialised_tags(self.tags.data)
proposal.requirements = self.requirements.data
proposal.length = self.length.data
proposal.notice_required = self.notice_required.data
Expand Down
63 changes: 63 additions & 0 deletions migrations/versions/0042c470500c_add_cfp_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""add cfp tags

Revision ID: 0042c470500c
Revises: 279d06c3289e
Create Date: 2023-12-22 10:08:06.559664

"""

# revision identifiers, used by Alembic.
revision = '0042c470500c'
down_revision = '279d06c3289e'

from alembic import op
import sqlalchemy as sa


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('proposal_tag_version',
sa.Column('proposal_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('tag_id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('proposal_id', 'tag_id', 'transaction_id', name=op.f('pk_proposal_tag_version'))
)
op.create_index(op.f('ix_proposal_tag_version_operation_type'), 'proposal_tag_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_proposal_tag_version_transaction_id'), 'proposal_tag_version', ['transaction_id'], unique=False)
op.create_table('tag',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tag', sa.String(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_tag')),
sa.UniqueConstraint('tag', name=op.f('uq_tag_tag'))
)
op.create_table('tag_version',
sa.Column('id', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('tag', sa.String(), autoincrement=False, nullable=False),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('id', 'transaction_id', name=op.f('pk_tag_version'))
)
op.create_index(op.f('ix_tag_version_operation_type'), 'tag_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_tag_version_transaction_id'), 'tag_version', ['transaction_id'], unique=False)
op.create_table('proposal_tag',
sa.Column('proposal_id', sa.Integer(), nullable=False),
sa.Column('tag_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['proposal_id'], ['proposal.id'], name=op.f('fk_proposal_tag_proposal_id_proposal')),
sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], name=op.f('fk_proposal_tag_tag_id_tag')),
sa.PrimaryKeyConstraint('proposal_id', 'tag_id', name=op.f('pk_proposal_tag'))
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('proposal_tag')
op.drop_index(op.f('ix_tag_version_transaction_id'), table_name='tag_version')
op.drop_index(op.f('ix_tag_version_operation_type'), table_name='tag_version')
op.drop_table('tag_version')
op.drop_table('tag')
op.drop_index(op.f('ix_proposal_tag_version_transaction_id'), table_name='proposal_tag_version')
op.drop_index(op.f('ix_proposal_tag_version_operation_type'), table_name='proposal_tag_version')
op.drop_table('proposal_tag_version')
# ### end Alembic commands ###
8 changes: 8 additions & 0 deletions models/cfp.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from main import db
from .user import User
from .cfp_tag import ProposalTag
from . import BaseModel


Expand Down Expand Up @@ -370,6 +371,13 @@ class Proposal(BaseModel):
notice_required = db.Column(db.String)
private_notes = db.Column(db.String)

tags = db.relationship(
"Tag",
backref="proposals",
cascade="all",
secondary=ProposalTag,
)

# Flags
needs_help = db.Column(db.Boolean, nullable=False, default=False)
needs_money = db.Column(db.Boolean, nullable=False, default=False)
Expand Down
64 changes: 64 additions & 0 deletions models/cfp_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from main import db
import sqlalchemy
from . import BaseModel


DEFAULT_TAGS = [
"computing",
"film",
"health",
"misc",
"music",
"radio",
"robotics",
"science",
"security",
"trains",
"show & tell",
]


class Tag(BaseModel):
__versioned__: dict = {}
__tablename__ = "tag"

id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.String, nullable=False, unique=True)

def __init__(self, tag: str):
self.tag = tag.strip().lower()

def __str__(self):
return self.tag

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

@classmethod
def serialise_tags(self, tag_list: list["Tag"]) -> str:
return ",".join([str(t) for t in tag_list])

@classmethod
def parse_serialised_tags(cls, tag_str: str) -> list["Tag"]:
res = []
# tag_list = [t.strip().lower() for t in tag_str.split(",")]
tag_list = [t.strip().lower() for t in tag_str]
for tag_value in tag_list:
if len(tag_value) == 0:
continue
tag = cls.query.filter_by(tag=tag_value).one_or_none()
marksteward marked this conversation as resolved.
Show resolved Hide resolved
if tag:
res.append(tag)
else:
res.append(Tag(tag_value))
return res


ProposalTag: sqlalchemy.Table = db.Table(
"proposal_tag",
BaseModel.metadata,
db.Column(
"proposal_id", db.Integer, db.ForeignKey("proposal.id"), primary_key=True
),
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
)
2 changes: 2 additions & 0 deletions templates/cfp_review/_nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
<li role="separator" class="divider"></li>
{{ menuitem('Anon-blocked', ".proposals", proposal_counts['anon-blocked'], state='anon-blocked')}}
{{ menuitem('New', ".proposals", proposal_counts['new'], state='new')}}
<li role="separator" class="divider"></li>
{{ menuitem('Summary', ".proposals_summary")}}
</ul>
</li>
<li class="dropdown">
Expand Down
Loading
Loading