added pucks and samples

This commit is contained in:
GotthardG
2024-11-04 11:34:14 +01:00
parent a9b8925be8
commit 23e7ebb819
17 changed files with 378 additions and 112 deletions

0
backend/app/__init__.py Normal file
View File

View File

@ -0,0 +1,8 @@
def calculate_number_of_pucks(dewar):
return len(dewar.pucks) if dewar.pucks else 0
def calculate_number_of_samples(dewar):
if not dewar.pucks:
return 0
return sum(len(puck.positions) for puck in dewar.pucks)

View File

@ -0,0 +1 @@
from .data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples

View File

@ -1,5 +1,6 @@
from app.models import ContactPerson, Address, Dewar, Proposal, Shipment
from app.models import ContactPerson, Address, Dewar, Proposal, Shipment, Puck, Sample
from datetime import datetime
import random
contacts = [
ContactPerson(id=1, firstname="Frodo", lastname="Baggins", phone_number="123-456-7890",
@ -34,32 +35,34 @@ return_addresses = [
dewars = [
Dewar(
id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123', number_of_pucks=7, number_of_samples=70,
id='DEWAR001', dewar_name='Dewar One', tracking_number='TRACK123',
return_address_id=1, contact_person_id=1, status='Ready for Shipping',
ready_date=datetime.strptime('2023-09-30', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR001',
),
Dewar(
id='DEWAR002', dewar_name='Dewar Two', tracking_number='TRACK124', number_of_pucks=3, number_of_samples=33,
id='DEWAR002', dewar_name='Dewar Two', tracking_number='TRACK124',
return_address_id=2, contact_person_id=2, status='In Preparation',
ready_date=None, shipping_date=None, arrival_date=None, returning_date=None, qrcode='QR123DEWAR002',
),
Dewar(
id='DEWAR003', dewar_name='Dewar Three', tracking_number='TRACK125', number_of_pucks=7, number_of_samples=72,
id='DEWAR003', dewar_name='Dewar Three', tracking_number='TRACK125',
return_address_id=1, contact_person_id=3, status='Not Shipped',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=None, arrival_date=None,
returning_date=None, qrcode='QR123DEWAR003',
),
Dewar(
id='DEWAR004', dewar_name='Dewar Four', tracking_number='', number_of_pucks=7, number_of_samples=70,
id='DEWAR004', dewar_name='Dewar Four', tracking_number='',
return_address_id=1, contact_person_id=3, status='Delayed',
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'), shipping_date=datetime.strptime('2024-01-02', '%Y-%m-%d'),
ready_date=datetime.strptime('2024-01-01', '%Y-%m-%d'),
shipping_date=datetime.strptime('2024-01-02', '%Y-%m-%d'),
arrival_date=None, returning_date=None, qrcode='QR123DEWAR004',
),
Dewar(
id='DEWAR005', dewar_name='Dewar Five', tracking_number='', number_of_pucks=3, number_of_samples=30,
id='DEWAR005', dewar_name='Dewar Five', tracking_number='',
return_address_id=1, contact_person_id=3, status='Returned',
arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'), returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'),
arrival_date=datetime.strptime('2024-01-03', '%Y-%m-%d'),
returning_date=datetime.strptime('2024-01-07', '%Y-%m-%d'),
qrcode='QR123DEWAR005',
),
]
@ -96,4 +99,51 @@ shipments = [
shipment_name='Shipment from Mordor', shipment_status='In Transit', contact_person_id=5,
proposal_id=5, return_address_id=1, comments='Contains the one ring', dewars=specific_dewars3
),
]
]
pucks = [
Puck(id=1, puck_name="PUCK001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR001'),
Puck(id=2, puck_name="PUCK002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR001'),
Puck(id=3, puck_name="PUCK003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR001'),
Puck(id=4, puck_name="PUCK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR001'),
Puck(id=5, puck_name="PUCK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR001'),
Puck(id=6, puck_name="PUCK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR001'),
Puck(id=7, puck_name="PUCK007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR001'),
Puck(id=8, puck_name="PK001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR002'),
Puck(id=9, puck_name="PK002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR002'),
Puck(id=10, puck_name="PK003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR002'),
Puck(id=11, puck_name="PK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR002'),
Puck(id=12, puck_name="PK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR002'),
Puck(id=13, puck_name="PK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR002'),
Puck(id=14, puck_name="P001", puck_type="Unipuck", puck_location_in_dewar=1, positions=[], dewar_id='DEWAR003'),
Puck(id=15, puck_name="P002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR003'),
Puck(id=16, puck_name="P003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR003'),
Puck(id=17, puck_name="P004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR003'),
Puck(id=18, puck_name="P005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR003'),
Puck(id=19, puck_name="P006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR003'),
Puck(id=20, puck_name="P007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR003'),
Puck(id=21, puck_name="PC002", puck_type="Unipuck", puck_location_in_dewar=2, positions=[], dewar_id='DEWAR004'),
Puck(id=22, puck_name="PC003", puck_type="Unipuck", puck_location_in_dewar=3, positions=[], dewar_id='DEWAR004'),
Puck(id=23, puck_name="PC004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR004'),
Puck(id=24, puck_name="PC005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR004'),
Puck(id=25, puck_name="PC006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR004'),
Puck(id=26, puck_name="PC007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR004'),
Puck(id=27, puck_name="PKK004", puck_type="Unipuck", puck_location_in_dewar=4, positions=[], dewar_id='DEWAR005'),
Puck(id=28, puck_name="PKK005", puck_type="Unipuck", puck_location_in_dewar=5, positions=[], dewar_id='DEWAR005'),
Puck(id=29, puck_name="PKK006", puck_type="Unipuck", puck_location_in_dewar=6, positions=[], dewar_id='DEWAR005'),
Puck(id=30, puck_name="PKK007", puck_type="Unipuck", puck_location_in_dewar=7, positions=[], dewar_id='DEWAR005')
]
samples = []
sample_id_counter = 1
for puck in pucks:
positions_with_samples = random.randint(1, 16)
occupied_positions = random.sample(range(1, 17), positions_with_samples)
for pos in range(1, 17):
if pos in occupied_positions:
sample = Sample(id=sample_id_counter, sample_name=f"Sample{sample_id_counter:03}", puck_id=puck.id)
puck.positions.append(sample)
samples.append(sample)
sample_id_counter += 1

View File

@ -3,7 +3,7 @@ from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" # Use appropriate path or database URL
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@ -21,19 +21,20 @@ def get_db():
def init_db():
# Import inside function to avoid circular dependency
from app import models
# Import models inside function to avoid circular dependency
from app import models
Base.metadata.create_all(bind=engine)
def load_sample_data(session: Session):
# Import inside function to avoid circular dependency
from app.data import contacts, return_addresses, dewars, proposals, shipments
# Import models inside function to avoid circular dependency
from app.data import contacts, return_addresses, dewars, proposals, shipments, pucks, samples
from app import models # Ensure these imports are correct
from app import models
# If any data already exists, skip seeding
if session.query(models.ContactPerson).first():
return
session.add_all(contacts + return_addresses + dewars + proposals + shipments)
session.commit()
session.add_all(contacts + return_addresses + dewars + proposals + shipments + pucks + samples)
session.commit()

View File

@ -1,7 +1,9 @@
from app.database import init_db
def initialize_database():
init_db()
if __name__ == "__main__":
initialize_database()
initialize_database()

View File

@ -3,7 +3,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import address, contact, proposal, dewar, shipment, upload
from app.routers import address, contact, proposal, dewar, shipment, upload, puck
from app.database import Base, engine, SessionLocal, load_sample_data
app = FastAPI()
@ -36,6 +36,7 @@ app.include_router(proposal.router, prefix="/proposals", tags=["proposals"])
app.include_router(dewar.router, prefix="/dewars", tags=["dewars"])
app.include_router(shipment.router, prefix="/shipments", tags=["shipments"])
app.include_router(upload.router, tags=["upload"]) # Removed the trailing '/' from the prefix
app.include_router(puck.router, prefix="/pucks", tags=["pucks"])
if __name__ == "__main__":
import uvicorn

View File

@ -1,6 +1,8 @@
from sqlalchemy import Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import relationship
from app.database import Base
from app.calculations import calculate_number_of_pucks, calculate_number_of_samples
class Shipment(Base):
__tablename__ = "shipments"
@ -19,6 +21,7 @@ class Shipment(Base):
proposal = relationship("Proposal", back_populates="shipments")
dewars = relationship("Dewar", back_populates="shipment")
class ContactPerson(Base):
__tablename__ = "contact_persons"
@ -30,6 +33,7 @@ class ContactPerson(Base):
shipments = relationship("Shipment", back_populates="contact_person")
class Address(Base):
__tablename__ = "addresses"
@ -41,14 +45,13 @@ class Address(Base):
shipments = relationship("Shipment", back_populates="return_address")
class Dewar(Base):
__tablename__ = "dewars"
id = Column(String, primary_key=True, index=True)
dewar_name = Column(String)
tracking_number = Column(String)
number_of_pucks = Column(Integer)
number_of_samples = Column(Integer)
status = Column(String)
ready_date = Column(Date, nullable=True)
shipping_date = Column(Date, nullable=True)
@ -56,12 +59,22 @@ class Dewar(Base):
returning_date = Column(Date, nullable=True)
qrcode = Column(String)
shipment_id = Column(String, ForeignKey("shipments.shipment_id"))
return_address_id = Column(Integer, ForeignKey("addresses.id")) # Added
contact_person_id = Column(Integer, ForeignKey("contact_persons.id")) # Added
return_address_id = Column(Integer, ForeignKey("addresses.id"))
contact_person_id = Column(Integer, ForeignKey("contact_persons.id"))
shipment = relationship("Shipment", back_populates="dewars")
return_address = relationship("Address")
contact_person = relationship("ContactPerson")
pucks = relationship("Puck", back_populates="dewar")
@property
def number_of_pucks(self) -> int:
return calculate_number_of_pucks(self)
@property
def number_of_samples(self) -> int:
return calculate_number_of_samples(self)
class Proposal(Base):
__tablename__ = "proposals"
@ -69,4 +82,25 @@ class Proposal(Base):
id = Column(Integer, primary_key=True, index=True)
number = Column(String)
shipments = relationship("Shipment", back_populates="proposal")
shipments = relationship("Shipment", back_populates="proposal")
class Puck(Base):
__tablename__ = 'pucks'
id = Column(String, primary_key=True)
puck_name = Column(String)
puck_type = Column(String)
puck_location_in_dewar = Column(Integer)
dewar_id = Column(String, ForeignKey('dewars.id')) # Note: changed to String
positions = relationship("Sample", back_populates="puck")
dewar = relationship("Dewar", back_populates="pucks")
class Sample(Base):
__tablename__ = 'samples'
id = Column(Integer, primary_key=True)
sample_name = Column(String)
puck_id = Column(Integer, ForeignKey('pucks.id'))
puck = relationship("Puck", back_populates="positions")

View File

@ -0,0 +1,7 @@
from .address import router as address_router
from .contact import router as contact_router
from .proposal import router as proposal_router
from .dewar import router as dewar_router
from .shipment import router as shipment_router
__all__ = ["address_router", "contact_router", "proposal_router", "dewar_router", "shipment_router"]

View File

@ -1,17 +1,19 @@
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from typing import List
import uuid
from app.schemas import Dewar as DewarSchema, DewarCreate
from app.models import Dewar as DewarModel
from app.schemas import Dewar as DewarSchema, DewarCreate, DewarUpdate, Sample as SampleSchema, Puck as PuckSchema
from app.models import Dewar as DewarModel, Puck as PuckModel, Sample as SampleModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[DewarSchema])
async def get_dewars(db: Session = Depends(get_db)):
return db.query(DewarModel).all()
@router.post("/", response_model=DewarSchema, status_code=status.HTTP_201_CREATED)
async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> DewarSchema:
dewar_id = f'DEWAR-{uuid.uuid4().hex[:8].upper()}'
@ -35,4 +37,32 @@ async def create_dewar(dewar: DewarCreate, db: Session = Depends(get_db)) -> Dew
db.add(db_dewar)
db.commit()
db.refresh(db_dewar)
return db_dewar
return db_dewar
@router.get("/{dewar_id}", response_model=DewarSchema)
async def get_dewar(dewar_id: str, db: Session = Depends(get_db)):
dewar = db.query(DewarModel).options(
joinedload(DewarModel.pucks).joinedload(PuckModel.positions)
).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
return dewar
@router.put("/{dewar_id}", response_model=DewarSchema)
async def update_dewar(dewar_id: str, dewar_update: DewarUpdate, db: Session = Depends(get_db)) -> DewarSchema:
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
for key, value in dewar_update.dict(exclude_unset=True).items():
setattr(dewar, key, value)
db.commit()
db.refresh(dewar)
return dewar

View File

@ -0,0 +1,63 @@
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from typing import List
import uuid
from app.schemas import Puck as PuckSchema, PuckCreate, PuckUpdate
from app.models import Puck as PuckModel, Sample as SampleModel
from app.dependencies import get_db
router = APIRouter()
@router.get("/", response_model=List[PuckSchema])
async def get_pucks(db: Session = Depends(get_db)):
return db.query(PuckModel).all()
@router.get("/{puck_id}", response_model=PuckSchema)
async def get_puck(puck_id: str, db: Session = Depends(get_db)):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
return puck
@router.post("/", response_model=PuckSchema, status_code=status.HTTP_201_CREATED)
async def create_puck(puck: PuckCreate, db: Session = Depends(get_db)) -> PuckSchema:
puck_id = f'PUCK-{uuid.uuid4().hex[:8].upper()}'
db_puck = PuckModel(
id=puck_id,
puck_name=puck.puck_name,
puck_type=puck.puck_type,
puck_location_in_dewar=puck.puck_location_in_dewar,
dewar_id=puck.dewar_id
)
db.add(db_puck)
db.commit()
db.refresh(db_puck)
return db_puck
@router.put("/{puck_id}", response_model=PuckSchema)
async def update_puck(puck_id: str, updated_puck: PuckUpdate, db: Session = Depends(get_db)):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
for key, value in updated_puck.dict(exclude_unset=True).items():
setattr(puck, key, value)
db.commit()
db.refresh(puck)
return puck
@router.delete("/{puck_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_puck(puck_id: str, db: Session = Depends(get_db)):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck:
raise HTTPException(status_code=404, detail="Puck not found")
db.delete(puck)
db.commit()
return

View File

@ -7,6 +7,7 @@ from datetime import date
from app.models import Shipment as ShipmentModel, ContactPerson as ContactPersonModel, Address as AddressModel, Proposal as ProposalModel, Dewar as DewarModel
from app.schemas import ShipmentCreate, Shipment as ShipmentSchema, DewarUpdate, ContactPerson as ContactPersonSchema
from app.schemas import Sample as SampleSchema
from app.database import get_db
from app.crud import get_shipments, get_shipment_by_id
@ -149,4 +150,33 @@ async def remove_dewar_from_shipment(shipment_id: str, dewar_id: str, db: Sessio
@router.get("/contact_persons", response_model=List[ContactPersonSchema])
async def get_shipment_contact_persons(db: Session = Depends(get_db)):
contact_persons = db.query(ContactPersonModel).all()
return contact_persons
return contact_persons
@router.get("/{shipment_id}/samples", response_model=List[SampleSchema])
def get_samples_in_shipment(shipment_id: str, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if shipment is None:
raise HTTPException(status_code=404, detail="Shipment not found")
samples = []
for dewar in shipment.dewars:
for puck in dewar.pucks:
samples.extend(puck.positions)
return samples
@router.get("/{shipment_id}/dewars/{dewar_id}/samples", response_model=List[SampleSchema])
def get_samples_in_dewar(shipment_id: str, dewar_id: str, db: Session = Depends(get_db)):
shipment = db.query(ShipmentModel).filter(ShipmentModel.shipment_id == shipment_id).first()
if shipment is None:
raise HTTPException(status_code=404, detail="Shipment not found")
dewar = db.query(DewarModel).filter(DewarModel.id == dewar_id, DewarModel.shipment_id == shipment_id).first()
if dewar is None:
raise HTTPException(status_code=404, detail="Dewar not found in shipment")
samples = []
for puck in dewar.pucks:
samples.extend(puck.positions)
return samples

View File

@ -3,7 +3,7 @@ from pydantic import BaseModel, EmailStr, constr
from datetime import date
# Base class for Contact Person
# Contact Person schemas
class ContactPersonBase(BaseModel):
firstname: str
lastname: str
@ -11,12 +11,10 @@ class ContactPersonBase(BaseModel):
email: EmailStr
# Create schema for Contact Person
class ContactPersonCreate(ContactPersonBase):
pass
# Response schema for Contact Person with ID
class ContactPerson(ContactPersonBase):
id: int
@ -24,7 +22,7 @@ class ContactPerson(ContactPersonBase):
from_attributes = True
# Create schema for Address
# Address schemas
class AddressCreate(BaseModel):
street: str
city: str
@ -32,7 +30,6 @@ class AddressCreate(BaseModel):
country: str
# Response schema for Address with ID
class Address(AddressCreate):
id: int
@ -40,8 +37,44 @@ class Address(AddressCreate):
from_attributes = True
# Create schema for Dewar
class DewarCreate(BaseModel):
# Sample schemas
class Sample(BaseModel):
id: int
sample_name: str
class Config:
from_attributes = True
# Puck schemas
class PuckBase(BaseModel):
puck_name: str
puck_type: str
puck_location_in_dewar: int
class PuckCreate(PuckBase):
positions: List[int] = []
class PuckUpdate(BaseModel):
puck_name: Optional[str] = None
puck_type: Optional[str] = None
puck_location_in_dewar: Optional[int] = None
dewar_id: Optional[int] = None
positions: Optional[List[int]] = None
class Puck(PuckBase):
id: int
positions: List[Sample] = []
class Config:
from_attributes = True
# Dewar schemas
class DewarBase(BaseModel):
dewar_name: str
tracking_number: str
number_of_pucks: int
@ -56,28 +89,37 @@ class DewarCreate(BaseModel):
return_address_id: Optional[int]
# Response schema for Dewar
class Dewar(BaseModel):
class DewarCreate(DewarBase):
pass
class Dewar(DewarBase):
id: str
dewar_name: str
tracking_number: str
number_of_pucks: int
number_of_samples: int
status: str
ready_date: Optional[date]
shipping_date: Optional[date]
arrival_date: Optional[date]
returning_date: Optional[date]
qrcode: str
shipment_id: Optional[str]
contact_person: Optional[ContactPerson]
return_address: Optional[Address]
pucks: Optional[List[Puck]] = []
class Config:
from_attributes = True
# Proposal schema
class DewarUpdate(BaseModel):
dewar_name: Optional[str] = None
tracking_number: Optional[str] = None
number_of_pucks: Optional[int] = None
number_of_samples: Optional[int] = None
status: Optional[str] = None
ready_date: Optional[date] = None
shipping_date: Optional[date] = None
arrival_date: Optional[date] = None
returning_date: Optional[date] = None
qrcode: Optional[str] = None
contact_person_id: Optional[int] = None
address_id: Optional[int] = None
# Proposal schemas
class Proposal(BaseModel):
id: int
number: str
@ -86,7 +128,7 @@ class Proposal(BaseModel):
from_attributes = True
# Response schema for Shipment
# Shipment schemas
class Shipment(BaseModel):
shipment_id: str
shipment_name: str
@ -101,20 +143,6 @@ class Shipment(BaseModel):
class Config:
from_attributes = True
class DewarUpdate(BaseModel):
dewar_id: str
dewar_name: Optional[str] = None
tracking_number: Optional[str] = None
number_of_pucks: Optional[int] = None
number_of_samples: Optional[int] = None
status: Optional[str] = None
ready_date: Optional[date] = None
shipping_date: Optional[date] = None
arrival_date: Optional[date] = None
returning_date: Optional[date] = None
qrcode: Optional[str] = None
contact_person_id: Optional[int] = None
address_id: Optional[int] = None # Added
class ShipmentCreate(BaseModel):
shipment_name: str

11
backend/app/utils.py Normal file
View File

@ -0,0 +1,11 @@
from app.models import Dewar
def calculate_number_of_pucks(dewar: Dewar) -> int:
return len(dewar.pucks) if dewar.pucks else 0
def calculate_number_of_samples(dewar: Dewar) -> int:
if not dewar.pucks:
return 0
return sum(len(puck.positions) for puck in dewar.pucks)